diff --git a/Cargo.lock b/Cargo.lock index 038b62f22..cbf704912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,7 @@ dependencies = [ "derive_builder_fork_arti", "derive_more 2.0.1", "educe", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "futures", "hostname-validator", "humantime", @@ -300,36 +300,59 @@ dependencies = [ "once_cell", "postage", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "thiserror 2.0.12", "time 0.3.41", - "tor-async-utils", - "tor-basic-utils", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-chanmgr", "tor-circmgr", - "tor-config", - "tor-config-path", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config-path 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-dirmgr", - "tor-error", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-guardmgr", "tor-hsclient", - "tor-hscrypto", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-hsservice", - "tor-keymgr", + "tor-keymgr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] +[[package]] +name = "arti-rpc-client-core" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d4ceb10e4abdbe7708ceed407460b1076756d2d35bd450ad2614fe86c3847c" +dependencies = [ + "caret 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "derive_more 2.0.1", + "educe", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding", + "rand 0.9.2", + "serde", + "serde_json", + "thiserror 2.0.12", + "tor-config-path 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-rpc-connect", + "tor-socksproto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "void", +] + [[package]] name = "ascii" version = "1.1.0" @@ -1534,6 +1557,12 @@ dependencies = [ "serde", ] +[[package]] +name = "caret" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061dc3258f029feaf9ff02b43c6af5ea67a7dfaed5d2aef36204c812e614ef9c" + [[package]] name = "caret" version = "0.5.3" @@ -2497,6 +2526,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.11", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -2937,6 +2979,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "domain" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd50aea158e9a57c9c9075ca7a3dfa4c08d9a468b405832383876f9df85379b" +dependencies = [ + "bytes", + "octseq", + "pin-project-lite", + "rand 0.8.5", + "time 0.3.41", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -3530,6 +3585,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-mistrust" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198b8f9ab4cff63b5c91e9e64edd4e6b43cd7fe7a52519a03c6c32ea0acfa557" +dependencies = [ + "derive_builder_fork_arti", + "dirs", + "libc", + "pwd-grp", + "serde", + "thiserror 2.0.12", + "walkdir", +] + [[package]] name = "fs-mistrust" version = "0.10.0" @@ -5908,8 +5978,9 @@ dependencies = [ "tokio-test", "tor-cell", "tor-hsservice", + "tor-interface", "tor-proto", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "tracing-subscriber", ] @@ -6436,7 +6507,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-test", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tower 0.5.2", "tower-http 0.6.6", "tracing", @@ -7115,6 +7186,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "octseq" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" +dependencies = [ + "bytes", +] + [[package]] name = "oid-registry" version = "0.6.1" @@ -7145,6 +7225,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "oneshot-fused-workaround" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2948fd2414b613f9a97f8401270bd5d7638265ab940475cdbcfa28a0273d58" +dependencies = [ + "futures", +] + [[package]] name = "oneshot-fused-workaround" version = "0.2.3" @@ -8643,6 +8732,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +[[package]] +name = "retry-error" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce97442758392c7e2a7716e06c514de75f0fe4b5a4b76e14ba1e5edfb7ba3512" + [[package]] name = "retry-error" version = "0.6.5" @@ -9101,6 +9196,19 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "safelog" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4e1c994fbc7521a5003e5c1c54304654ea0458881e777f6e2638520c2de8c5" +dependencies = [ + "derive_more 2.0.1", + "educe", + "either", + "fluid-let", + "thiserror 2.0.12", +] + [[package]] name = "same-file" version = "1.0.6" @@ -9640,6 +9748,20 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92761393ee4dc3ff8f4af487bd58f4307c9329bbedea02cac0089ad9c411e153" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot 0.12.4", + "serial_test_derive 0.9.0", +] + [[package]] name = "serial_test" version = "3.2.0" @@ -9651,7 +9773,19 @@ dependencies = [ "once_cell", "parking_lot 0.12.4", "scc", - "serial_test_derive", + "serial_test_derive 3.2.0", +] + +[[package]] +name = "serial_test_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6f5d1c3087fb119617cff2966fe3808a80e5eb59a8c1601d5994d66f4346a5" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -9978,6 +10112,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "git+https://github.com/nabijaczleweli/rust-socks#a1182ee5024d06cba2104a1145dd648e10a90468" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "softbuffer" version = "0.4.6" @@ -10535,7 +10679,7 @@ dependencies = [ "serde_cbor", "serde_json", "serde_with 1.14.0", - "serial_test", + "serial_test 3.2.0", "sha2 0.10.9", "sigma_fun", "sqlx", @@ -10556,7 +10700,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tower 0.4.13", "tower-http 0.3.5", "tracing", @@ -11574,6 +11718,20 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tokio-mpmc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffadf729b08a5df966b11daa6faee399f72a4ddb00125c0e8853aa4e0f08006c" +dependencies = [ + "crossbeam-queue", + "futures", + "rand 0.9.2", + "thiserror 1.0.69", + "tokio", + "tracing", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -11779,6 +11937,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +[[package]] +name = "tor-async-utils" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bee2c4a8ea4cfe533bf284eef89d26f53ed0145854c54471734cb36e451f3e" +dependencies = [ + "derive-deftly 1.1.0", + "educe", + "futures", + "oneshot-fused-workaround 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pin-project", + "postage", + "thiserror 2.0.12", + "void", +] + [[package]] name = "tor-async-utils" version = "0.32.0" @@ -11787,13 +11961,31 @@ dependencies = [ "derive-deftly 1.1.0", "educe", "futures", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "postage", "thiserror 2.0.12", "void", ] +[[package]] +name = "tor-basic-utils" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a4e9e4da7ce0f089aee75808db143c4d547ca6d4ffd5b15a1cb042c3082928" +dependencies = [ + "derive_more 2.0.1", + "hex", + "itertools 0.14.0", + "libc", + "paste", + "rand 0.9.2", + "rand_chacha 0.9.0", + "slab", + "smallvec", + "thiserror 2.0.12", +] + [[package]] name = "tor-basic-utils" version = "0.32.0" @@ -11812,6 +12004,24 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "tor-bytes" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a4a8401219d99b460c9bc001386366a4c50dc881a0abf346f04c1785f7e06a" +dependencies = [ + "bytes", + "derive-deftly 1.1.0", + "digest 0.10.7", + "educe", + "getrandom 0.3.3", + "safelog 0.4.8", + "thiserror 2.0.12", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize", +] + [[package]] name = "tor-bytes" version = "0.32.0" @@ -11822,10 +12032,10 @@ dependencies = [ "digest 0.10.7", "educe", "getrandom 0.3.3", - "safelog", + "safelog 0.4.7", "thiserror 2.0.12", - "tor-error", - "tor-llcrypto", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "zeroize", ] @@ -11837,7 +12047,7 @@ dependencies = [ "amplify", "bitflags 2.9.1", "bytes", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive-deftly 1.1.0", "derive_more 2.0.1", "educe", @@ -11845,32 +12055,48 @@ dependencies = [ "rand 0.9.2", "smallvec", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-cert", - "tor-error", - "tor-hscrypto", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-protover", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "void", ] +[[package]] +name = "tor-cert" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abc35321d207c3ffbfdc37feb0a9e186555ee49dfaa8079027115ce44491ff2" +dependencies = [ + "caret 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_builder_fork_arti", + "derive_more 2.0.1", + "digest 0.10.7", + "thiserror 2.0.12", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-checkable 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-cert" version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive_builder_fork_arti", "derive_more 2.0.1", "digest 0.10.7", "thiserror 2.0.12", - "tor-bytes", - "tor-checkable", - "tor-llcrypto", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -11879,34 +12105,46 @@ version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ "async-trait", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive_builder_fork_arti", "derive_more 2.0.1", "educe", "futures", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "postage", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-config", - "tor-error", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-netdir", "tor-proto", - "tor-rtcompat", - "tor-socksproto", - "tor-units", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-socksproto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] +[[package]] +name = "tor-checkable" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e994401be86ecdaa8b17b176cd1562ddddc6778a47afd751e6db99ccadb8e6" +dependencies = [ + "humantime", + "signature 2.2.0", + "thiserror 2.0.12", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-checkable" version = "0.32.0" @@ -11915,7 +12153,7 @@ dependencies = [ "humantime", "signature 2.2.0", "thiserror 2.0.12", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -11936,35 +12174,68 @@ dependencies = [ "humantime-serde", "itertools 0.14.0", "once_cell", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "rand 0.9.2", - "retry-error", - "safelog", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "safelog 0.4.7", "serde", "static_assertions", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-chanmgr", - "tor-config", - "tor-error", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-guardmgr", "tor-linkspec", "tor-memquota", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", "tor-relay-selection", - "tor-rtcompat", - "tor-units", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", "weak-table", ] +[[package]] +name = "tor-config" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3280e6e26a30f94d752d55d273c307d2b819971ff4b830101d816990f9a5ec36" +dependencies = [ + "amplify", + "cfg-if", + "derive-deftly 1.1.0", + "derive_builder_fork_arti", + "educe", + "either", + "figment", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures", + "itertools 0.14.0", + "notify", + "paste", + "postage", + "regex", + "serde", + "serde-value", + "serde_ignored", + "strum 0.27.2", + "thiserror 2.0.12", + "toml 0.8.23", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-rtcompat 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "void", +] + [[package]] name = "tor-config" version = "0.32.0" @@ -11977,7 +12248,7 @@ dependencies = [ "educe", "either", "figment", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "futures", "itertools 0.14.0", "notify", @@ -11990,13 +12261,27 @@ dependencies = [ "strum 0.27.2", "thiserror 2.0.12", "toml 0.8.23", - "tor-basic-utils", - "tor-error", - "tor-rtcompat", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] +[[package]] +name = "tor-config-path" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5347bcbe96c660694fe52fb76e852d982d73fe0d92f4c4cb9eaa8427a5d52f17" +dependencies = [ + "directories", + "serde", + "shellexpand", + "thiserror 2.0.12", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-general-addr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-config-path" version = "0.32.0" @@ -12006,8 +12291,8 @@ dependencies = [ "serde", "shellexpand", "thiserror 2.0.12", - "tor-error", - "tor-general-addr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-general-addr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -12018,7 +12303,7 @@ dependencies = [ "digest 0.10.7", "hex", "thiserror 2.0.12", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -12038,13 +12323,13 @@ dependencies = [ "memchr", "thiserror 2.0.12", "tor-circmgr", - "tor-error", - "tor-hscrypto", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdoc", "tor-proto", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", ] @@ -12060,7 +12345,7 @@ dependencies = [ "digest 0.10.7", "educe", "event-listener", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "fslock", "futures", "hex", @@ -12068,12 +12353,12 @@ dependencies = [ "humantime-serde", "itertools 0.14.0", "memmap2", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "paste", "postage", "rand 0.9.2", "rusqlite", - "safelog", + "safelog 0.4.7", "scopeguard", "serde", "serde_json", @@ -12082,25 +12367,42 @@ dependencies = [ "strum 0.27.2", "thiserror 2.0.12", "time 0.3.41", - "tor-async-utils", - "tor-basic-utils", - "tor-checkable", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-circmgr", - "tor-config", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-consdiff", "tor-dirclient", - "tor-error", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-guardmgr", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", ] +[[package]] +name = "tor-error" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ae19c74564749c54e14e532ffb15f84807f734d17f452bb3ffb8b1957f06a2" +dependencies = [ + "derive_more 2.0.1", + "futures", + "paste", + "retry-error 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "static_assertions", + "strum 0.27.2", + "thiserror 2.0.12", + "tracing", + "void", +] + [[package]] name = "tor-error" version = "0.32.0" @@ -12109,7 +12411,7 @@ dependencies = [ "derive_more 2.0.1", "futures", "paste", - "retry-error", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "static_assertions", "strum 0.27.2", "thiserror 2.0.12", @@ -12117,6 +12419,17 @@ dependencies = [ "void", ] +[[package]] +name = "tor-general-addr" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad9c6e9147f4ee644c80c3b044813cf93a3f802279c49b06aac2f4f33555877" +dependencies = [ + "derive_more 2.0.1", + "thiserror 2.0.12", + "void", +] + [[package]] name = "tor-general-addr" version = "0.32.0" @@ -12144,27 +12457,27 @@ dependencies = [ "humantime-serde", "itertools 0.14.0", "num_enum", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "postage", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "strum 0.27.2", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-error", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-relay-selection", - "tor-rtcompat", - "tor-units", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", ] @@ -12180,37 +12493,65 @@ dependencies = [ "either", "futures", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "postage", "rand 0.9.2", - "retry-error", - "safelog", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "safelog 0.4.7", "slotmap-careful", "strum 0.27.2", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-checkable", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-circmgr", - "tor-config", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-dirclient", - "tor-error", - "tor-hscrypto", - "tor-keymgr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-keymgr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", ] +[[package]] +name = "tor-hscrypto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7469efe5d22466fcaaeec9506bf03426ce59c03ee1b8a7c8b316830b153b40a" +dependencies = [ + "data-encoding", + "derive_more 2.0.1", + "digest 0.10.7", + "hex", + "humantime", + "itertools 0.14.0", + "paste", + "rand 0.9.2", + "safelog 0.4.8", + "serde", + "signature 2.2.0", + "subtle", + "thiserror 2.0.12", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-key-forge 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-units 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "void", +] + [[package]] name = "tor-hscrypto" version = "0.32.0" @@ -12226,18 +12567,18 @@ dependencies = [ "itertools 0.14.0", "paste", "rand 0.9.2", - "safelog", + "safelog 0.4.7", "serde", "signature 2.2.0", "subtle", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-error", - "tor-key-forge", - "tor-llcrypto", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-key-forge 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "void", "zeroize", ] @@ -12256,7 +12597,7 @@ dependencies = [ "derive_more 2.0.1", "digest 0.10.7", "educe", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "futures", "growable-bloom-filter", "hex", @@ -12264,41 +12605,100 @@ dependencies = [ "itertools 0.14.0", "k12", "once_cell", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "postage", "rand 0.9.2", "rand_core 0.9.3", - "retry-error", - "safelog", + "retry-error 0.6.5 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "safelog 0.4.7", "serde", "serde_with 3.14.0", "strum 0.27.2", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", "tor-circmgr", - "tor-config", - "tor-config-path", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config-path 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-dirclient", - "tor-error", - "tor-hscrypto", - "tor-keymgr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-keymgr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-log-ratelim", "tor-netdir", "tor-netdoc", - "tor-persist", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-proto", "tor-protover", "tor-relay-selection", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] +[[package]] +name = "tor-interface" +version = "0.5.0" +dependencies = [ + "anyhow", + "arti-client", + "arti-rpc-client-core", + "curve25519-dalek 4.1.3", + "data-encoding", + "data-encoding-macro", + "domain", + "hmac", + "idna", + "rand 0.9.2", + "rand_core 0.9.3", + "regex", + "serial_test 0.9.0", + "sha1", + "sha2 0.10.9", + "sha3", + "signature 1.6.4", + "socks", + "static_assertions", + "thiserror 1.0.69", + "tokio", + "tokio-mpmc", + "tokio-stream", + "tor-cell", + "tor-config 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-hsservice", + "tor-keymgr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-proto", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "which", + "zeroize", +] + +[[package]] +name = "tor-key-forge" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6697b58f1518757b975993d345a261387eb7a86c730cb542ad1ea68284155eaa" +dependencies = [ + "derive-deftly 1.1.0", + "derive_more 2.0.1", + "downcast-rs 2.0.1", + "paste", + "rand 0.9.2", + "signature 2.2.0", + "ssh-key", + "thiserror 2.0.12", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-cert 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-checkable 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-key-forge" version = "0.32.0" @@ -12312,11 +12712,49 @@ dependencies = [ "signature 2.2.0", "ssh-key", "thiserror 2.0.12", - "tor-bytes", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-llcrypto", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", +] + +[[package]] +name = "tor-keymgr" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d765c0fd6b1910b427feb1b604fbc85e5768332e9be1e670bf64e698c92606" +dependencies = [ + "amplify", + "arrayvec", + "cfg-if", + "derive-deftly 1.1.0", + "derive_builder_fork_arti", + "derive_more 2.0.1", + "downcast-rs 2.0.1", + "dyn-clone", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glob-match", + "humantime", + "inventory", + "itertools 0.14.0", + "rand 0.9.2", + "serde", + "signature 2.2.0", + "ssh-key", + "thiserror 2.0.12", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-config 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-config-path 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-hscrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-key-forge 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-llcrypto 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-persist 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "walkdir", + "zeroize", ] [[package]] @@ -12332,7 +12770,7 @@ dependencies = [ "derive_more 2.0.1", "downcast-rs 2.0.1", "dyn-clone", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "glob-match", "humantime", "inventory", @@ -12342,15 +12780,15 @@ dependencies = [ "signature 2.2.0", "ssh-key", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-config-path", - "tor-error", - "tor-hscrypto", - "tor-key-forge", - "tor-llcrypto", - "tor-persist", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config-path 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-key-forge 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-persist 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "visibility", "walkdir", @@ -12364,25 +12802,62 @@ source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df195 dependencies = [ "base64ct", "by_address", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive-deftly 1.1.0", "derive_builder_fork_arti", "derive_more 2.0.1", "hex", "itertools 0.14.0", - "safelog", + "safelog 0.4.7", "serde", "serde_with 3.14.0", "strum 0.27.2", "thiserror 2.0.12", - "tor-basic-utils", - "tor-bytes", - "tor-config", - "tor-llcrypto", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-memquota", "tor-protover", ] +[[package]] +name = "tor-llcrypto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "992c49dd4c285c52594858c0e92afe96531203dbe9bb29cfbe6937d94bb3c7ad" +dependencies = [ + "aes", + "base64ct", + "ctr", + "curve25519-dalek 4.1.3", + "der-parser 10.0.0", + "derive_more 2.0.1", + "digest 0.10.7", + "ed25519-dalek 2.2.0", + "educe", + "getrandom 0.3.3", + "hex", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_core 0.6.4", + "rand_core 0.9.3", + "rand_jitter", + "rdrand", + "rsa", + "safelog 0.4.8", + "serde", + "sha1", + "sha2 0.10.9", + "sha3", + "signature 2.2.0", + "subtle", + "thiserror 2.0.12", + "visibility", + "x25519-dalek", + "zeroize", +] + [[package]] name = "tor-llcrypto" version = "0.32.0" @@ -12407,7 +12882,7 @@ dependencies = [ "rand_jitter", "rdrand", "rsa", - "safelog", + "safelog 0.4.7", "serde", "sha1", "sha2 0.10.9", @@ -12429,8 +12904,8 @@ dependencies = [ "futures", "humantime", "thiserror 2.0.12", - "tor-error", - "tor-rtcompat", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "weak-table", ] @@ -12452,12 +12927,12 @@ dependencies = [ "slotmap-careful", "static_assertions", "thiserror 2.0.12", - "tor-async-utils", - "tor-basic-utils", - "tor-config", - "tor-error", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-log-ratelim", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] @@ -12482,14 +12957,14 @@ dependencies = [ "strum 0.27.2", "thiserror 2.0.12", "time 0.3.41", - "tor-basic-utils", - "tor-error", - "tor-hscrypto", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-netdoc", "tor-protover", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "typed-index-collections", ] @@ -12521,22 +12996,49 @@ dependencies = [ "thiserror 2.0.12", "time 0.3.41", "tinystr", - "tor-basic-utils", - "tor-bytes", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-cert", - "tor-checkable", - "tor-error", - "tor-hscrypto", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-protover", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "void", "weak-table", "zeroize", ] +[[package]] +name = "tor-persist" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fabc9ba76dbe0ca3b254ed73480455a337c1941904f375d583efcdc57966f98" +dependencies = [ + "derive-deftly 1.1.0", + "derive_more 2.0.1", + "filetime", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fslock", + "futures", + "itertools 0.14.0", + "oneshot-fused-workaround 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "paste", + "sanitize-filename", + "serde", + "serde_json", + "thiserror 2.0.12", + "time 0.3.41", + "tor-async-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "void", +] + [[package]] name = "tor-persist" version = "0.32.0" @@ -12546,21 +13048,21 @@ dependencies = [ "derive-deftly 1.1.0", "derive_more 2.0.1", "filetime", - "fs-mistrust", + "fs-mistrust 0.10.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "fslock", "fslock-guard", "futures", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "paste", "sanitize-filename", "serde", "serde_json", "thiserror 2.0.12", "time 0.3.41", - "tor-async-utils", - "tor-basic-utils", - "tor-error", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] @@ -12574,7 +13076,7 @@ dependencies = [ "asynchronous-codec 0.7.0", "bitvec", "bytes", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "cfg-if", "cipher", "coarsetime", @@ -12589,12 +13091,12 @@ dependencies = [ "hkdf", "hmac", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "postage", "rand 0.9.2", "rand_core 0.9.3", - "safelog", + "safelog 0.4.7", "slotmap-careful", "smallvec", "static_assertions", @@ -12603,23 +13105,23 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-util", - "tor-async-utils", - "tor-basic-utils", - "tor-bytes", + "tor-async-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-cell", - "tor-cert", - "tor-checkable", - "tor-config", - "tor-error", - "tor-hscrypto", + "tor-cert 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-checkable 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-config 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-hscrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", - "tor-llcrypto", + "tor-llcrypto 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-log-ratelim", "tor-memquota", "tor-protover", - "tor-rtcompat", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-rtmock", - "tor-units", + "tor-units 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "typenum", "visibility", @@ -12632,11 +13134,11 @@ name = "tor-protover" version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "paste", "serde_with 3.14.0", "thiserror 2.0.12", - "tor-bytes", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", ] [[package]] @@ -12646,12 +13148,63 @@ source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df195 dependencies = [ "rand 0.9.2", "serde", - "tor-basic-utils", + "tor-basic-utils 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tor-linkspec", "tor-netdir", "tor-netdoc", ] +[[package]] +name = "tor-rpc-connect" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985904e33200b9d2ef6e98863430855ecc45cd96c9811d839151c9651fdd306d" +dependencies = [ + "base16ct", + "cfg-if", + "derive_builder_fork_arti", + "derive_more 2.0.1", + "fs-mistrust 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.9.2", + "safelog 0.4.8", + "serde", + "serde_with 3.14.0", + "subtle", + "thiserror 2.0.12", + "tiny-keccak", + "toml 0.8.23", + "tor-basic-utils 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-config-path 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-general-addr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "zeroize", +] + +[[package]] +name = "tor-rtcompat" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75969f63c5147af49753e1c03339d342bc3e53e7412518e52702cc417cff729" +dependencies = [ + "async-trait", + "async_executors", + "asynchronous-codec 0.7.0", + "coarsetime", + "derive_more 2.0.1", + "dyn-clone", + "educe", + "futures", + "hex", + "libc", + "paste", + "pin-project", + "thiserror 2.0.12", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-general-addr 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing", + "void", +] + [[package]] name = "tor-rtcompat" version = "0.32.0" @@ -12675,8 +13228,8 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-util", - "tor-error", - "tor-general-addr", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-general-addr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "void", ] @@ -12695,34 +13248,62 @@ dependencies = [ "futures", "humantime", "itertools 0.14.0", - "oneshot-fused-workaround", + "oneshot-fused-workaround 0.2.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "pin-project", "priority-queue", "slotmap-careful", "strum 0.27.2", "thiserror 2.0.12", - "tor-error", - "tor-general-addr", - "tor-rtcompat", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-general-addr 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-rtcompat 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "tracing", "tracing-test", "void", ] +[[package]] +name = "tor-socksproto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b7d2a9b7394b8f2c683d282f4a0da08c056ed23b3bb9afdb84cb92d554c77b" +dependencies = [ + "amplify", + "caret 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "derive-deftly 1.1.0", + "educe", + "safelog 0.4.8", + "subtle", + "thiserror 2.0.12", + "tor-bytes 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tor-error 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tor-socksproto" version = "0.32.0" source = "git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841#18111286b5830cda88af5df1950b5e24ee5a8841" dependencies = [ "amplify", - "caret", + "caret 0.5.3 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", "derive-deftly 1.1.0", "educe", - "safelog", + "safelog 0.4.7", "subtle", "thiserror 2.0.12", - "tor-bytes", - "tor-error", + "tor-bytes 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", + "tor-error 0.32.0 (git+https://github.com/eigenwallet/arti?rev=18111286b5830cda88af5df1950b5e24ee5a8841)", +] + +[[package]] +name = "tor-units" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f2bd3dc4f5defec5d4b9d152d911a3a852d08409558dd927ec8eb28e20f9de" +dependencies = [ + "derive_more 2.0.1", + "serde", + "thiserror 2.0.12", ] [[package]] @@ -13247,6 +13828,7 @@ dependencies = [ "serde", "serde_json", "swap", + "swap-env", "tauri", "tauri-build", "tauri-plugin-cli", @@ -13833,6 +14415,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "whoami" version = "1.6.0" diff --git a/libp2p-rendezvous-server/Cargo.toml b/libp2p-rendezvous-server/Cargo.toml index aae092818..5ddcee08f 100644 --- a/libp2p-rendezvous-server/Cargo.toml +++ b/libp2p-rendezvous-server/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" anyhow = "1" futures = { workspace = true } libp2p = { workspace = true, default-features = false, features = ["rendezvous", "tcp", "yamux", "dns", "noise", "ping", "websocket", "tokio", "macros"] } -libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } +libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service", "legacy-tor-provider"] } tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs", "net", "io-util"] } tor-hsservice = { workspace = true } tracing = { workspace = true, features = ["attributes"] } diff --git a/libp2p-tor/Cargo.toml b/libp2p-tor/Cargo.toml index 2dfc376cb..210aea82d 100644 --- a/libp2p-tor/Cargo.toml +++ b/libp2p-tor/Cargo.toml @@ -15,6 +15,8 @@ thiserror = { workspace = true } tokio = { workspace = true } arti-client = { workspace = true, features = ["tokio", "rustls", "onion-service-client", "static-sqlite"] } +# tor-interface = { git = "https://github.com/nabijaczleweli/gosling", rev = "32988e5770c12f1b48b865c158509473123eae90", optional = true } +tor-interface = { path = "../tor-interface", optional = true } libp2p = { workspace = true, features = ["tokio", "tcp", "tls"] } data-encoding = { version = "2.6.0" } @@ -37,11 +39,18 @@ listen-onion-service = [ "dep:tor-cell", "dep:tor-proto", ] +arti-client-tor-provider = ["tor-interface", "tor-interface/arti-client-tor-provider"] +legacy-tor-provider = ["tor-interface", "tor-interface/legacy-tor-provider"] +mock-tor-provider = ["tor-interface", "tor-interface/mock-tor-provider"] [[example]] name = "ping-onion" required-features = ["listen-onion-service"] +[[example]] +name = "ping-onion-tor-interface" +required-features = ["tor-interface"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/libp2p-tor/README.md b/libp2p-tor/README.md index ecb7ecbc5..36af54031 100644 --- a/libp2p-tor/README.md +++ b/libp2p-tor/README.md @@ -43,6 +43,13 @@ let mut transport = libp2p_tor::TorTransport::bootstrapped().await?; // we have achieved tor connection let _conn = transport.dial(address)?.await?; ``` +```rust +let address = "/dns/www.torproject.org/tcp/1000".parse()?; +let mut provider = libp2p_tor_interface::tor_interface::/* whichever one you want */; +let mut transport = libp2p_tor_interface::TorInterfaceTransport::from_provider(Default::default(), Arc::new(Mutex::new(provider)), None); +// we have achieved tor connection +let _conn = transport.dial(address)?.await?; +``` ### About diff --git a/libp2p-tor/examples/ping-onion-tor-interface.rs b/libp2p-tor/examples/ping-onion-tor-interface.rs new file mode 100644 index 000000000..b5710f186 --- /dev/null +++ b/libp2p-tor/examples/ping-onion-tor-interface.rs @@ -0,0 +1,146 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Ping-Onion example +//! +//! See ../src/tutorial.rs for a step-by-step guide building the example below. +//! +//! This example requires two seperate computers, one of which has to be reachable from the +//! internet. +//! +//! On the first computer run: +//! ```sh +//! cargo run --example ping +//! ``` +//! +//! It will print the PeerId and the listening addresses, e.g. `Listening on +//! "/ip4/0.0.0.0/tcp/24915"` +//! +//! Make sure that the first computer is reachable under one of these ip addresses and port. +//! +//! On the second computer run: +//! ```sh +//! cargo run --example ping-onion -- /ip4/123.45.67.89/tcp/24915 +//! ``` +//! +//! The two nodes establish a connection, negotiate the ping protocol +//! and begin pinging each other over Tor. + +use futures::StreamExt; +use libp2p::core::upgrade::Version; +use libp2p::Transport; +use libp2p::{ + core::muxing::StreamMuxerBox, + identity, noise, + swarm::{NetworkBehaviour, SwarmEvent}, + yamux, Multiaddr, PeerId, SwarmBuilder, +}; +use std::error::Error; +use std::sync::{Arc, Mutex}; +use tor_interface::tor_crypto::Ed25519PrivateKey; + +/// Create a transport +/// Returns a tuple of the transport and the onion address we can instruct it to listen on +async fn onion_transport( + keypair: identity::Keypair, +) -> Result< + ( + libp2p::core::transport::Boxed<(PeerId, libp2p::core::muxing::StreamMuxerBox)>, + Multiaddr, + ), + Box, +> { + let provider = libp2p_community_tor::tor_interface::legacy_tor_client::LegacyTorClient::new( + libp2p_community_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().expect("Configure $TOR_... to talk to"))?; + + let mut transport = libp2p_community_tor::TorInterfaceTransport::from_provider( + libp2p_community_tor::AddressConversion::IpAndDns, Arc::new(Mutex::new(provider)), None)?; + + let onion_listen_address = transport.add_onion_service(&Ed25519PrivateKey::generate(), 999, None, None).unwrap(); + + let auth_upgrade = noise::Config::new(&keypair)?; + let multiplex_upgrade = yamux::Config::default(); + + let transport = transport + .boxed() + .upgrade(Version::V1) + .authenticate(auth_upgrade) + .multiplex(multiplex_upgrade) + .map(|(peer, muxer), _| (peer, StreamMuxerBox::new(muxer))) + .boxed(); + + Ok((transport, onion_listen_address)) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(local_key.public()); + + println!("Local peer id: {local_peer_id}"); + + let (transport, onion_listen_address) = onion_transport(local_key).await?; + + let mut swarm = SwarmBuilder::with_new_identity() + .with_tokio() + .with_other_transport(|_| transport) + .unwrap() + .with_behaviour(|_| Behaviour { + ping: libp2p::ping::Behaviour::default(), + }) + .unwrap() + .build(); + + // Dial the peer identified by the multi-address given as the second + // command-line argument, if any. + if let Some(addr) = std::env::args().nth(1) { + let remote: Multiaddr = addr.parse()?; + swarm.dial(remote)?; + println!("Dialed {addr}") + } else { + // If we are not dialing, we need to listen + // Tell the swarm to listen on a specific onion address + swarm.listen_on(onion_listen_address).unwrap(); + } + + loop { + match swarm.select_next_some().await { + SwarmEvent::ConnectionEstablished { + endpoint, peer_id, .. + } => { + println!("Connection established with {peer_id} on {endpoint:?}"); + } + SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { + println!("Outgoing connection error with {peer_id:?}: {error:?}"); + } + SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"), + SwarmEvent::Behaviour(event) => println!("{event:?}"), + _ => {} + } + } +} + +/// Our network behaviour. +#[derive(NetworkBehaviour)] +struct Behaviour { + ping: libp2p::ping::Behaviour, +} diff --git a/libp2p-tor/src/address.rs b/libp2p-tor/src/arti/address.rs similarity index 100% rename from libp2p-tor/src/address.rs rename to libp2p-tor/src/arti/address.rs diff --git a/libp2p-tor/src/arti/mod.rs b/libp2p-tor/src/arti/mod.rs new file mode 100644 index 000000000..2d30a6cbd --- /dev/null +++ b/libp2p-tor/src/arti/mod.rs @@ -0,0 +1,413 @@ +use arti_client::{TorClient, TorClientBuilder}; +use futures::future::BoxFuture; +use libp2p::{ + core::transport::{ListenerId, TransportEvent}, + Multiaddr, Transport, TransportError, +}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use thiserror::Error; +use tor_rtcompat::tokio::TokioRustlsRuntime; + +// We only need these imports if the `listen-onion-service` feature is enabled +#[cfg(feature = "listen-onion-service")] +use std::collections::HashMap; +#[cfg(feature = "listen-onion-service")] +use std::str::FromStr; +#[cfg(feature = "listen-onion-service")] +use tor_cell::relaycell::msg::{Connected, End, EndReason}; +#[cfg(feature = "listen-onion-service")] +use tor_hsservice::{ + handle_rend_requests, status::OnionServiceStatus, HsId, OnionServiceConfig, + RunningOnionService, StreamRequest, +}; +#[cfg(feature = "listen-onion-service")] +use tor_proto::stream::IncomingStreamRequest; + +mod address; +mod provider; + +use address::{dangerous_extract, safe_extract}; +pub use provider::TokioTorStream; + +pub type TorError = arti_client::Error; + +type PendingUpgrade = BoxFuture<'static, Result>; +#[cfg(feature = "listen-onion-service")] +type OnionServiceStream = futures::stream::BoxStream<'static, StreamRequest>; +#[cfg(feature = "listen-onion-service")] +type OnionServiceStatusStream = futures::stream::BoxStream<'static, OnionServiceStatus>; + +/// Struct representing an onion address we are listening on for libp2p connections. +#[cfg(feature = "listen-onion-service")] +struct TorListener { + #[allow(dead_code)] // We need to own this to keep the RunningOnionService alive + /// The onion service we are listening on + service: Arc, + /// The stream of status updates for the onion service + status_stream: OnionServiceStatusStream, + /// The stream incoming [`StreamRequest`]s + request_stream: OnionServiceStream, + + /// The port we are listening on + port: u16, + /// The onion address we are listening on + onion_address: Multiaddr, + /// Whether we have already announced this address + announced: bool, +} + +/// Mode of address conversion. +/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details +#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum AddressConversion { + /// Uses only DNS for address resolution (default). + #[default] + DnsOnly, + /// Uses IP and DNS for addresses. + IpAndDns, +} + +pub struct TorTransport { + pub conversion_mode: AddressConversion, + + /// The Tor client. + client: Arc>, + + /// Onion services we are listening on. + #[cfg(feature = "listen-onion-service")] + listeners: HashMap, + + /// Onion services we are running but currently not listening on + #[cfg(feature = "listen-onion-service")] + services: Vec<(Arc, OnionServiceStream)>, +} + +impl TorTransport { + /// Creates a new `TorClientBuilder`. + /// + /// # Panics + /// Panics if the current runtime is not a `TokioRustlsRuntime`. + pub fn builder() -> TorClientBuilder { + let runtime = + TokioRustlsRuntime::current().expect("Couldn't get the current tokio rustls runtime"); + TorClient::with_runtime(runtime) + } + + /// Creates a bootstrapped `TorTransport` + /// + /// # Errors + /// Could return error emitted during Tor bootstrap by Arti. + pub async fn bootstrapped() -> Result { + let builder = Self::builder(); + let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?; + ret.bootstrap().await?; + Ok(ret) + } + + /// Builds a `TorTransport` from an Arti `TorClientBuilder` but does not bootstrap it. + /// + /// # Errors + /// Could return error emitted during creation of the `TorClient`. + pub fn from_builder( + builder: &TorClientBuilder, + conversion_mode: AddressConversion, + ) -> Result { + let client = Arc::new(builder.create_unbootstrapped()?); + + Ok(Self::from_client(client, conversion_mode)) + } + + /// Builds a `TorTransport` from an existing Arti `TorClient`. + pub fn from_client( + client: Arc>, + conversion_mode: AddressConversion, + ) -> Self { + Self { + conversion_mode, + client, + #[cfg(feature = "listen-onion-service")] + listeners: HashMap::new(), + #[cfg(feature = "listen-onion-service")] + services: Vec::new(), + } + } + + /// Bootstraps the `TorTransport` into the Tor network. + /// + /// # Errors + /// Could return error emitted during bootstrap by Arti. + pub async fn bootstrap(&self) -> Result<(), TorError> { + self.client.bootstrap().await + } + + /// Set the address conversion mode + #[must_use] + pub fn with_address_conversion(mut self, conversion_mode: AddressConversion) -> Self { + self.conversion_mode = conversion_mode; + self + } + + /// Call this function to instruct the transport to listen on a specific onion address + /// You need to call this function **before** calling `listen_on` + /// + /// # Returns + /// Returns the Multiaddr of the onion address that the transport can be instructed to listen on + /// To actually listen on the address, you need to call [`listen_on`] with the returned address + /// + /// # Errors + /// Returns an error if we cannot get the onion address of the service + #[cfg(feature = "listen-onion-service")] + pub fn add_onion_service( + &mut self, + svc_cfg: OnionServiceConfig, + port: u16, + ) -> anyhow::Result { + let (service, request_stream) = self.client.launch_onion_service(svc_cfg)?; + let request_stream = Box::pin(handle_rend_requests(request_stream)); + + let multiaddr = service + .onion_address() + .ok_or_else(|| anyhow::anyhow!("Onion service has no onion address"))? + .to_multiaddr(port); + + self.services.push((service, request_stream)); + + Ok(multiaddr) + } +} + +#[derive(Debug, Error)] +pub enum TorTransportError { + #[error(transparent)] + Client(#[from] TorError), + #[cfg(feature = "listen-onion-service")] + #[error(transparent)] + Service(#[from] tor_hsservice::ClientError), + #[cfg(feature = "listen-onion-service")] + #[error("Stream closed before receiving data")] + StreamClosed, + #[cfg(feature = "listen-onion-service")] + #[error("Stream port does not match listener port")] + StreamPortMismatch, + #[cfg(feature = "listen-onion-service")] + #[error("Onion service is broken")] + Broken, +} + +#[cfg(feature = "listen-onion-service")] +trait HsIdExt { + fn to_multiaddr(&self, port: u16) -> Multiaddr; +} + +#[cfg(feature = "listen-onion-service")] +impl HsIdExt for HsId { + /// Convert an `HsId` to a `Multiaddr` + fn to_multiaddr(&self, port: u16) -> Multiaddr { + let onion_domain = self.to_string(); + let onion_without_dot_onion = onion_domain + .split('.') + .nth(0) + .expect("Display formatting of HsId to contain .onion suffix"); + let multiaddress_string = format!("/onion3/{onion_without_dot_onion}:{port}"); + + Multiaddr::from_str(&multiaddress_string) + .expect("A valid onion address to be convertible to a Multiaddr") + } +} + +impl Transport for TorTransport { + type Output = TokioTorStream; + type Error = TorTransportError; + type Dial = BoxFuture<'static, Result>; + type ListenerUpgrade = PendingUpgrade; + + #[cfg(not(feature = "listen-onion-service"))] + fn listen_on( + &mut self, + _id: ListenerId, + onion_address: Multiaddr, + ) -> Result<(), TransportError> { + // If the `listen-onion-service` feature is not enabled, we do not support listening + Err(TransportError::MultiaddrNotSupported(onion_address.clone())) + } + + #[cfg(feature = "listen-onion-service")] + fn listen_on( + &mut self, + id: ListenerId, + onion_address: Multiaddr, + ) -> Result<(), TransportError> { + // If the address is not an onion3 address, return an error + let Some(libp2p::multiaddr::Protocol::Onion3(address)) = onion_address.into_iter().nth(0) + else { + return Err(TransportError::MultiaddrNotSupported(onion_address.clone())); + }; + + // Find the running onion service that matches the requested address + // If we find it, remove it from [`services`] and insert it into [`listeners`] + let position = self + .services + .iter() + .position(|(service, _)| { + service.onion_address().map_or(false, |name| { + name.to_multiaddr(address.port()) == onion_address + }) + }) + .ok_or_else(|| TransportError::MultiaddrNotSupported(onion_address.clone()))?; + + let (service, request_stream) = self.services.remove(position); + + let status_stream = Box::pin(service.status_events()); + + self.listeners.insert( + id, + TorListener { + service, + request_stream, + onion_address: onion_address.clone(), + port: address.port(), + status_stream, + announced: false, + }, + ); + + Ok(()) + } + + // We do not support removing listeners if the `listen-onion-service` feature is not enabled + #[cfg(not(feature = "listen-onion-service"))] + fn remove_listener(&mut self, _id: ListenerId) -> bool { + false + } + + #[cfg(feature = "listen-onion-service")] + fn remove_listener(&mut self, id: ListenerId) -> bool { + // Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore) + // However, we will not stop the onion service itself because we might want to reuse it later + // The onion service will be stopped when the transport is dropped + if let Some(listener) = self.listeners.remove(&id) { + self.services + .push((listener.service, listener.request_stream)); + return true; + } + + false + } + + fn dial(&mut self, addr: Multiaddr) -> Result> { + let maybe_tor_addr = match self.conversion_mode { + AddressConversion::DnsOnly => safe_extract(&addr), + AddressConversion::IpAndDns => dangerous_extract(&addr), + }; + + let tor_address = + maybe_tor_addr.ok_or(TransportError::MultiaddrNotSupported(addr.clone()))?; + let onion_client = self.client.clone(); + + Ok(Box::pin(async move { + let stream = onion_client.connect(tor_address).await?; + + tracing::debug!(%addr, "Established connection to peer through Tor"); + + Ok(TokioTorStream::from(stream)) + })) + } + + fn dial_as_listener( + &mut self, + addr: Multiaddr, + ) -> Result> { + self.dial(addr) + } + + fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option { + None + } + + #[cfg(not(feature = "listen-onion-service"))] + fn poll( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + // If the `listen-onion-service` feature is not enabled, we do not support listening + Poll::Pending + } + + #[cfg(feature = "listen-onion-service")] + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + for (listener_id, listener) in &mut self.listeners { + // Check if the service has any new statuses + if let Poll::Ready(Some(status)) = listener.status_stream.as_mut().poll_next(cx) { + tracing::debug!( + status = ?status.state(), + address = listener.onion_address.to_string(), + "Onion service status changed" + ); + } + + // Check if we have already announced this address, if not, do it now + if !listener.announced { + listener.announced = true; + + // We announce the address here to the swarm even though we technically cannot guarantee + // that the address is reachable yet from the outside. We might not have registered the + // onion service fully yet (introduction points, hsdir, ...) + // + // However, we need to announce it now because otherwise libp2p might not poll the listener + // again and we will not be able to announce it later. + // TODO: Find out why this is the case, if this is intended behaviour or a bug + return Poll::Ready(TransportEvent::NewAddress { + listener_id: *listener_id, + listen_addr: listener.onion_address.clone(), + }); + } + + match listener.request_stream.as_mut().poll_next(cx) { + Poll::Ready(Some(request)) => { + let port = listener.port; + let upgrade: PendingUpgrade = Box::pin(async move { + // Check if the port matches what we expect + if let IncomingStreamRequest::Begin(begin) = request.request() { + if begin.port() != port { + // Reject the connection with CONNECTREFUSED + request + .reject(End::new_with_reason(EndReason::CONNECTREFUSED)) + .await?; + + return Err(TorTransportError::StreamPortMismatch); + } + } + + // Accept the stream and forward it to the swarm + let data_stream = request.accept(Connected::new_empty()).await?; + Ok(TokioTorStream::from(data_stream)) + }); + + return Poll::Ready(TransportEvent::Incoming { + listener_id: *listener_id, + upgrade, + local_addr: listener.onion_address.clone(), + send_back_addr: listener.onion_address.clone(), + }); + } + + // The stream has ended + // This means that the onion service was shut down, and we will not receive any more connections on it + Poll::Ready(None) => { + return Poll::Ready(TransportEvent::ListenerClosed { + listener_id: *listener_id, + reason: Ok(()), + }); + } + Poll::Pending => {} + } + } + + Poll::Pending + } +} diff --git a/libp2p-tor/src/provider.rs b/libp2p-tor/src/arti/provider.rs similarity index 100% rename from libp2p-tor/src/provider.rs rename to libp2p-tor/src/arti/provider.rs diff --git a/libp2p-tor/src/lib.rs b/libp2p-tor/src/lib.rs index 1c426080f..f15e135ce 100644 --- a/libp2p-tor/src/lib.rs +++ b/libp2p-tor/src/lib.rs @@ -38,7 +38,7 @@ //! This crate uses tokio with rustls for its runtime and TLS implementation. //! No other combinations are supported. //! -//! ## Example +//! ## Examples //! ```no_run //! use libp2p::core::Transport; //! # async fn test_func() -> Result<(), Box> { @@ -50,431 +50,28 @@ //! # } //! # tokio_test::block_on(test_func()); //! ``` +//! +//! ```no_run +//! use libp2p::core::Transport; +//! use std::sync::{Arc, Mutex}; +//! use libp2p_tor::tor_interface::tor_provider::TorProvider; +//! # async fn test_func() -> Result<(), Box> { +//! let address = "/dns/www.torproject.org/tcp/1000".parse()?; +//! let mut provider = libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClient::new( +//! libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().unwrap())?; +//! provider.bootstrap()?; +//! let mut transport = libp2p_tor::TorInterfaceTransport::from_provider(Default::default(), Arc::new(Mutex::new(provider)), None)?; +//! // we have achieved tor connection +//! let _conn = transport.dial(address)?.await?; +//! # Ok(()) +//! # } +//! # tokio_test::block_on(test_func()); +//! ``` -use arti_client::{TorClient, TorClientBuilder}; -use futures::future::BoxFuture; -use libp2p::{ - core::transport::{ListenerId, TransportEvent}, - Multiaddr, Transport, TransportError, -}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use thiserror::Error; -use tor_rtcompat::tokio::TokioRustlsRuntime; - -// We only need these imports if the `listen-onion-service` feature is enabled -#[cfg(feature = "listen-onion-service")] -use std::collections::HashMap; -#[cfg(feature = "listen-onion-service")] -use std::str::FromStr; -#[cfg(feature = "listen-onion-service")] -use tor_cell::relaycell::msg::{Connected, End, EndReason}; -#[cfg(feature = "listen-onion-service")] -use tor_hsservice::{ - handle_rend_requests, status::OnionServiceStatus, HsId, OnionServiceConfig, - RunningOnionService, StreamRequest, -}; -#[cfg(feature = "listen-onion-service")] -use tor_proto::stream::IncomingStreamRequest; - -mod address; -mod provider; - -use address::{dangerous_extract, safe_extract}; -pub use provider::TokioTorStream; - -pub type TorError = arti_client::Error; - -type PendingUpgrade = BoxFuture<'static, Result>; -#[cfg(feature = "listen-onion-service")] -type OnionServiceStream = futures::stream::BoxStream<'static, StreamRequest>; -#[cfg(feature = "listen-onion-service")] -type OnionServiceStatusStream = futures::stream::BoxStream<'static, OnionServiceStatus>; - -/// Struct representing an onion address we are listening on for libp2p connections. -#[cfg(feature = "listen-onion-service")] -struct TorListener { - #[allow(dead_code)] // We need to own this to keep the RunningOnionService alive - /// The onion service we are listening on - service: Arc, - /// The stream of status updates for the onion service - status_stream: OnionServiceStatusStream, - /// The stream incoming [`StreamRequest`]s - request_stream: OnionServiceStream, - - /// The port we are listening on - port: u16, - /// The onion address we are listening on - onion_address: Multiaddr, - /// Whether we have already announced this address - announced: bool, -} - -/// Mode of address conversion. -/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details -#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq, PartialOrd, Ord)] -pub enum AddressConversion { - /// Uses only DNS for address resolution (default). - #[default] - DnsOnly, - /// Uses IP and DNS for addresses. - IpAndDns, -} - -pub struct TorTransport { - pub conversion_mode: AddressConversion, - - /// The Tor client. - client: Arc>, - - /// Onion services we are listening on. - #[cfg(feature = "listen-onion-service")] - listeners: HashMap, - - /// Onion services we are running but currently not listening on - #[cfg(feature = "listen-onion-service")] - services: Vec<(Arc, OnionServiceStream)>, -} - -impl TorTransport { - /// Creates a new `TorClientBuilder`. - /// - /// # Panics - /// Panics if the current runtime is not a `TokioRustlsRuntime`. - pub fn builder() -> TorClientBuilder { - let runtime = - TokioRustlsRuntime::current().expect("Couldn't get the current tokio rustls runtime"); - TorClient::with_runtime(runtime) - } - - /// Creates a bootstrapped `TorTransport` - /// - /// # Errors - /// Could return error emitted during Tor bootstrap by Arti. - pub async fn bootstrapped() -> Result { - let builder = Self::builder(); - let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?; - ret.bootstrap().await?; - Ok(ret) - } - - /// Creates an unbootstrapped `TorTransport`. It will bootstrap in the background. - /// This can silently fail - pub async fn unbootstrapped() -> Result { - let builder = Self::builder(); - let ret = Self::from_builder(&builder, AddressConversion::DnsOnly)?; - let bootstrap_client = ret.client.clone(); - tokio::spawn(async move { - if let Err(e) = bootstrap_client.bootstrap().await { - tracing::error!("Tor bootstrap failed: {}", e); - } - }); - Ok(ret) - } - - /// Builds a `TorTransport` from an Arti `TorClientBuilder` but does not bootstrap it. - /// - /// # Errors - /// Could return error emitted during creation of the `TorClient`. - pub fn from_builder( - builder: &TorClientBuilder, - conversion_mode: AddressConversion, - ) -> Result { - let client = Arc::new(builder.create_unbootstrapped()?); - - Ok(Self::from_client(client, conversion_mode)) - } - - /// Builds a `TorTransport` from an existing Arti `TorClient`. - pub fn from_client( - client: Arc>, - conversion_mode: AddressConversion, - ) -> Self { - Self { - conversion_mode, - client, - #[cfg(feature = "listen-onion-service")] - listeners: HashMap::new(), - #[cfg(feature = "listen-onion-service")] - services: Vec::new(), - } - } - - /// Bootstraps the `TorTransport` into the Tor network. - /// - /// # Errors - /// Could return error emitted during bootstrap by Arti. - pub async fn bootstrap(&self) -> Result<(), TorError> { - self.client.bootstrap().await - } - - /// Set the address conversion mode - #[must_use] - pub fn with_address_conversion(mut self, conversion_mode: AddressConversion) -> Self { - self.conversion_mode = conversion_mode; - self - } - - /// Call this function to instruct the transport to listen on a specific onion address - /// You need to call this function **before** calling `listen_on` - /// - /// # Returns - /// Returns the Multiaddr of the onion address that the transport can be instructed to listen on - /// To actually listen on the address, you need to call [`listen_on`] with the returned address - /// - /// # Errors - /// Returns an error if we cannot get the onion address of the service - #[cfg(feature = "listen-onion-service")] - pub fn add_onion_service( - &mut self, - svc_cfg: OnionServiceConfig, - port: u16, - ) -> anyhow::Result { - let (service, request_stream) = self.client.launch_onion_service(svc_cfg)?; - let request_stream = Box::pin(handle_rend_requests(request_stream)); - - let multiaddr = service - .onion_address() - .ok_or_else(|| anyhow::anyhow!("Onion service has no onion address"))? - .to_multiaddr(port); - - self.services.push((service, request_stream)); - - Ok(multiaddr) - } -} - -#[derive(Debug, Error)] -pub enum TorTransportError { - #[error(transparent)] - Client(#[from] TorError), - #[cfg(feature = "listen-onion-service")] - #[error(transparent)] - Service(#[from] tor_hsservice::ClientError), - #[cfg(feature = "listen-onion-service")] - #[error("Stream closed before receiving data")] - StreamClosed, - #[cfg(feature = "listen-onion-service")] - #[error("Stream port does not match listener port")] - StreamPortMismatch, - #[cfg(feature = "listen-onion-service")] - #[error("Onion service is broken")] - Broken, -} - -#[cfg(feature = "listen-onion-service")] -trait HsIdExt { - fn to_multiaddr(&self, port: u16) -> Multiaddr; -} - -#[cfg(feature = "listen-onion-service")] -impl HsIdExt for HsId { - /// Convert an `HsId` to a `Multiaddr` - fn to_multiaddr(&self, port: u16) -> Multiaddr { - let onion_domain = self.to_string(); - let onion_without_dot_onion = onion_domain - .split('.') - .nth(0) - .expect("Display formatting of HsId to contain .onion suffix"); - let multiaddress_string = format!("/onion3/{onion_without_dot_onion}:{port}"); - - Multiaddr::from_str(&multiaddress_string) - .expect("A valid onion address to be convertible to a Multiaddr") - } -} - -impl Transport for TorTransport { - type Output = TokioTorStream; - type Error = TorTransportError; - type Dial = BoxFuture<'static, Result>; - type ListenerUpgrade = PendingUpgrade; - - #[cfg(not(feature = "listen-onion-service"))] - fn listen_on( - &mut self, - _id: ListenerId, - onion_address: Multiaddr, - ) -> Result<(), TransportError> { - // If the `listen-onion-service` feature is not enabled, we do not support listening - Err(TransportError::MultiaddrNotSupported(onion_address.clone())) - } - - #[cfg(feature = "listen-onion-service")] - fn listen_on( - &mut self, - id: ListenerId, - onion_address: Multiaddr, - ) -> Result<(), TransportError> { - // If the address is not an onion3 address, return an error - let Some(libp2p::multiaddr::Protocol::Onion3(address)) = onion_address.into_iter().nth(0) - else { - return Err(TransportError::MultiaddrNotSupported(onion_address.clone())); - }; - - // Find the running onion service that matches the requested address - // If we find it, remove it from [`services`] and insert it into [`listeners`] - let position = self - .services - .iter() - .position(|(service, _)| { - service.onion_address().map_or(false, |name| { - name.to_multiaddr(address.port()) == onion_address - }) - }) - .ok_or_else(|| TransportError::MultiaddrNotSupported(onion_address.clone()))?; - - let (service, request_stream) = self.services.remove(position); - - let status_stream = Box::pin(service.status_events()); - - self.listeners.insert( - id, - TorListener { - service, - request_stream, - onion_address: onion_address.clone(), - port: address.port(), - status_stream, - announced: false, - }, - ); - - Ok(()) - } - - // We do not support removing listeners if the `listen-onion-service` feature is not enabled - #[cfg(not(feature = "listen-onion-service"))] - fn remove_listener(&mut self, _id: ListenerId) -> bool { - false - } - - #[cfg(feature = "listen-onion-service")] - fn remove_listener(&mut self, id: ListenerId) -> bool { - // Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore) - // However, we will not stop the onion service itself because we might want to reuse it later - // The onion service will be stopped when the transport is dropped - if let Some(listener) = self.listeners.remove(&id) { - self.services - .push((listener.service, listener.request_stream)); - return true; - } - - false - } - - fn dial(&mut self, addr: Multiaddr) -> Result> { - let maybe_tor_addr = match self.conversion_mode { - AddressConversion::DnsOnly => safe_extract(&addr), - AddressConversion::IpAndDns => dangerous_extract(&addr), - }; - - let tor_address = - maybe_tor_addr.ok_or(TransportError::MultiaddrNotSupported(addr.clone()))?; - let onion_client = self.client.clone(); - - Ok(Box::pin(async move { - let stream = onion_client.connect(tor_address).await?; - - tracing::debug!(%addr, "Established connection to peer through Tor"); - - Ok(TokioTorStream::from(stream)) - })) - } - - fn dial_as_listener( - &mut self, - addr: Multiaddr, - ) -> Result> { - self.dial(addr) - } - - fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option { - None - } - - #[cfg(not(feature = "listen-onion-service"))] - fn poll( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - // If the `listen-onion-service` feature is not enabled, we do not support listening - Poll::Pending - } - - #[cfg(feature = "listen-onion-service")] - fn poll( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - for (listener_id, listener) in &mut self.listeners { - // Check if the service has any new statuses - if let Poll::Ready(Some(status)) = listener.status_stream.as_mut().poll_next(cx) { - tracing::debug!( - status = ?status.state(), - address = listener.onion_address.to_string(), - "Onion service status changed" - ); - } - - // Check if we have already announced this address, if not, do it now - if !listener.announced { - listener.announced = true; - - // We announce the address here to the swarm even though we technically cannot guarantee - // that the address is reachable yet from the outside. We might not have registered the - // onion service fully yet (introduction points, hsdir, ...) - // - // However, we need to announce it now because otherwise libp2p might not poll the listener - // again and we will not be able to announce it later. - // TODO: Find out why this is the case, if this is intended behaviour or a bug - return Poll::Ready(TransportEvent::NewAddress { - listener_id: *listener_id, - listen_addr: listener.onion_address.clone(), - }); - } - - match listener.request_stream.as_mut().poll_next(cx) { - Poll::Ready(Some(request)) => { - let port = listener.port; - let upgrade: PendingUpgrade = Box::pin(async move { - // Check if the port matches what we expect - if let IncomingStreamRequest::Begin(begin) = request.request() { - if begin.port() != port { - // Reject the connection with CONNECTREFUSED - request - .reject(End::new_with_reason(EndReason::CONNECTREFUSED)) - .await?; - - return Err(TorTransportError::StreamPortMismatch); - } - } - - // Accept the stream and forward it to the swarm - let data_stream = request.accept(Connected::new_empty()).await?; - Ok(TokioTorStream::from(data_stream)) - }); - - return Poll::Ready(TransportEvent::Incoming { - listener_id: *listener_id, - upgrade, - local_addr: listener.onion_address.clone(), - send_back_addr: listener.onion_address.clone(), - }); - } - - // The stream has ended - // This means that the onion service was shut down, and we will not receive any more connections on it - Poll::Ready(None) => { - return Poll::Ready(TransportEvent::ListenerClosed { - listener_id: *listener_id, - reason: Ok(()), - }); - } - Poll::Pending => {} - } - } +mod arti; +pub use arti::*; - Poll::Pending - } -} +#[cfg(feature="tor-interface")] +mod tor; +#[cfg(feature="tor-interface")] +pub use tor::*; diff --git a/libp2p-tor/src/tor/address.rs b/libp2p-tor/src/tor/address.rs new file mode 100644 index 000000000..441006b16 --- /dev/null +++ b/libp2p-tor/src/tor/address.rs @@ -0,0 +1,161 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +use libp2p::{core::multiaddr::Protocol, Multiaddr}; +use tor_interface::tor_provider::{OnionAddr, OnionAddrV3, TargetAddr, DomainAddr}; +use tor_interface::tor_crypto::{V3OnionServiceId, Ed25519PublicKey}; +use std::net::SocketAddr; + +/// "Dangerously" extract a Tor address from the provided [`Multiaddr`]. +/// +/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details around the safety / privacy considerations. +pub fn dangerous_extract(multiaddr: &Multiaddr) -> Option { + if let Some(tor_addr) = safe_extract(multiaddr) { + return Some(tor_addr); + } + + let mut protocols = multiaddr.into_iter(); + + try_to_socket_addr(&protocols.next()?, &protocols.next()?) +} + +/// "Safely" extract a Tor address from the provided [`Multiaddr`]. +/// +/// Refer tor [arti_client::TorAddr](https://docs.rs/arti-client/latest/arti_client/struct.TorAddr.html) for details around the safety / privacy considerations. +pub fn safe_extract(multiaddr: &Multiaddr) -> Option { + let mut protocols = multiaddr.into_iter(); + + let (dom, port) = (protocols.next()?, protocols.next()); + try_to_domain_and_port(&dom, &port) +} + +fn try_to_domain_and_port<'a>( + maybe_domain: &'a Protocol, + maybe_port: &Option, +) -> Option { + match (maybe_domain, maybe_port) { + ( + Protocol::Dns(domain) | Protocol::Dns4(domain) | Protocol::Dns6(domain), + Some(Protocol::Tcp(port)), + ) => Some(TargetAddr::Domain(DomainAddr::try_from((domain.to_string(), *port)).ok()?.into())), + (Protocol::Onion3(domain), _) => + Some(TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new(V3OnionServiceId::from_public_key(&Ed25519PublicKey::from_raw(domain.hash()[..32].try_into().unwrap()).ok()?), domain.port())))), + _ => None, + } +} + +fn try_to_socket_addr(maybe_ip: &Protocol, maybe_port: &Protocol) -> Option { + match (maybe_ip, maybe_port) { + (Protocol::Ip4(ip), Protocol::Tcp(port)) => Some(TargetAddr::Socket(SocketAddr::from((*ip, *port)))), + (Protocol::Ip6(ip), Protocol::Tcp(port)) => Some(TargetAddr::Socket(SocketAddr::from((*ip, *port)))), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tor_interface::tor_provider::TargetAddr; + use std::str::FromStr; + + #[test] + fn extract_correct_address_from_dns() { + let addresses = [ + "/dns/ip.tld/tcp/10".parse().unwrap(), + "/dns4/dns.ip4.tld/tcp/11".parse().unwrap(), + "/dns6/dns.ip6.tld/tcp/12".parse().unwrap(), + "/onion3/cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd:13".parse().unwrap(), + ]; + + let actual = addresses + .iter() + .filter_map(safe_extract) + .collect::>(); + + assert_eq!( + &[ + TargetAddr::from_str("ip.tld:10").unwrap(), + TargetAddr::from_str("dns.ip4.tld:11").unwrap(), + TargetAddr::from_str("dns.ip6.tld:12").unwrap(), + TargetAddr::from_str("cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion:13").unwrap(), + ], + actual.as_slice() + ); + } + + #[test] + fn extract_correct_address_from_ips() { + let addresses = [ + "/ip4/127.0.0.1/tcp/10".parse().unwrap(), + "/ip6/::1/tcp/10".parse().unwrap(), + ]; + + let actual = addresses + .iter() + .filter_map(dangerous_extract) + .collect::>(); + + assert_eq!( + &[ + TargetAddr::from_str("127.0.0.1:10").unwrap(), + TargetAddr::from_str("[::1]:10").unwrap(), + ], + actual.as_slice() + ); + } + + #[test] + fn dangerous_extract_works_on_domains_too() { + let addresses = [ + "/dns/ip.tld/tcp/10".parse().unwrap(), + "/ip4/127.0.0.1/tcp/10".parse().unwrap(), + "/ip6/::1/tcp/10".parse().unwrap(), + ]; + + let actual = addresses + .iter() + .filter_map(dangerous_extract) + .collect::>(); + + assert_eq!( + &[ + TargetAddr::from_str("ip.tld:10").unwrap(), + TargetAddr::from_str("127.0.0.1:10").unwrap(), + TargetAddr::from_str("[::1]:10").unwrap(), + ], + actual.as_slice() + ); + } + + #[test] + fn detect_incorrect_address() { + let addresses = [ + "/tcp/10/udp/12".parse().unwrap(), + "/dns/ip.tld/dns4/ip.tld/dns6/ip.tld".parse().unwrap(), + "/tcp/10/ip4/1.1.1.1".parse().unwrap(), + ]; + + let all_correct = addresses.iter().map(safe_extract).all(|res| res.is_none()); + + assert!( + all_correct, + "During the parsing of the faulty addresses, there was an incorrectness" + ); + } +} diff --git a/libp2p-tor/src/tor/mod.rs b/libp2p-tor/src/tor/mod.rs new file mode 100644 index 000000000..7b6921418 --- /dev/null +++ b/libp2p-tor/src/tor/mod.rs @@ -0,0 +1,349 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![warn(clippy::pedantic)] +#![deny(unsafe_code)] + +use futures::future::BoxFuture; +use tor_interface::tor_provider::{self, CircuitToken, TcpOnionListener, TcpOrUnixOnionStream, TorProvider, OnionListener, OnionAddr}; +use tor_interface::tor_crypto::{V3OnionServiceId, Ed25519PrivateKey, X25519PublicKey}; +use libp2p::{ + core::transport::{ListenerId, TransportEvent}, + Multiaddr, Transport, TransportError, +}; + +use std::collections::{BTreeSet, HashMap}; +use std::str::FromStr; +use tokio::net::TcpListener; + +use std::pin::Pin; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::net::SocketAddr; +use std::task::{Context, Poll}; +use thiserror::Error; + +mod address; +mod provider; + +use address::{dangerous_extract, safe_extract}; +pub use provider::OnionStreamStream; + +use crate::AddressConversion; + +pub use tor_interface; + +/// Get a [`TorProvider`](`tor_provider::TorProvider`) from [`tor_interface`] +pub struct TorInterfaceTransport { + pub conversion_mode: AddressConversion, + pub provider: Arc>, + pub circuit: Option, + + /// Onion services we are listening on. + listeners: HashMap, + + /// Onion services we are running (implicitly excluded if ListenerId present) + services: Vec<(TcpOnionListener, Option)>, + + /// Services yet to be announced + waiting_to_announce: HashMap, + + event_backlog: Vec, + + /// Persistent list of services we already publish + /// + /// Tor delineates services by onion but libp2p does it by onion:port + published_services: BTreeSet, +} + +#[derive(Debug, Error)] +pub enum TorInterfaceTransportError { + #[error(transparent)] + Client(#[from] tor_provider::Error), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +fn lock(m: &Mutex) -> MutexGuard<'_, T> { + match m.lock() { + Ok(o) => o, + Err(e) => e.into_inner(), + } +} + +fn bootstrap(provider: &mut T) -> Result<(), tor_provider::Error> { + match provider.bootstrap() { + Err(tor_provider::Error::Generic(s)) if s.ends_with(" already bootstrapped") => Ok(()), + res @ Ok(_) | res @ Err(_) => res, + } +} + +impl> TorInterfaceTransport { + /// Creates a new `TorClientBuilder`. + pub fn from_provider( + conversion_mode: AddressConversion, + provider: Arc>, + circuit: Option + ) -> Result { + bootstrap(&mut *lock(&provider))?; + Ok(Self { + conversion_mode: conversion_mode, + provider: provider, + circuit: circuit, + listeners: HashMap::new(), + services: Vec::new(), + waiting_to_announce: Default::default(), + event_backlog: Default::default(), + published_services: Default::default(), + }) + } + + /// Call this function to instruct the transport to listen on a specific onion address + /// You need to call this function **before** calling `listen_on` + /// + /// # Returns + /// Returns the Multiaddr of the onion address that the transport can be instructed to listen on + /// To actually listen on the address, you need to call [`listen_on()`] with the returned address + /// + /// # Blocks + /// If listening fails with an `LegacyTorNotBootstrapped` error, + /// `bootstrap()`s the provider and awaits bootstrap confirtmation + /// + /// # Errors + /// Returns an error if we couldn't talk to the tor daemon + pub fn add_onion_service( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorised_clients: Option<&[X25519PublicKey]>, + socket_addr: Option, + ) -> anyhow::Result { + let ol = self.listener_or_bootstrap(|p| p.listener(private_key, virt_port, authorised_clients, socket_addr))?; + ol.set_nonblocking(true)?; + + self.services.push((ol, None)); + + let svid = V3OnionServiceId::from_private_key(&private_key); + let multiaddr = svid.to_multiaddr(virt_port); + + Ok(multiaddr) + } + + fn listener_or_bootstrap Result>(&mut self, mut f: F) -> Result { + loop { + let attempt = f(&mut lock(&self.provider)); // Moving this into the match clause deadlocks (Guard still borrowed) + match attempt { + Err(tor_provider::Error::Generic(s)) if s.ends_with(" not bootstrapped") => { + bootstrap(&mut *lock(&self.provider))?; + self.event_backlog.extend(lock(&self.provider).update()?); + } + res @ Ok(_) | res @ Err(_) => return res, + } + } + } +} + +trait HsIdExt { + fn to_multiaddr(&self, port: u16) -> Multiaddr; +} + +impl HsIdExt for V3OnionServiceId { + /// Convert an `V3OnionServiceId` to a `Multiaddr` + fn to_multiaddr(&self, port: u16) -> Multiaddr { + // The internal representation of V3OnionServiceId is 52 characters, so we can't re-use it here. + let multiaddress_string = format!("/onion3/{self}:{port}"); + + Multiaddr::from_str(&multiaddress_string) + .expect("A valid onion address to be convertible to a Multiaddr") + } +} + +trait OnionAddrExt { + fn to_multiaddr(&self) -> Multiaddr; +} + +impl OnionAddrExt for OnionAddr { + fn to_multiaddr(&self) -> Multiaddr { + let OnionAddr::V3(v3) = self; + v3.service_id().to_multiaddr(v3.virt_port()) + } +} + +#[cfg(test)] +#[test] +fn to_multiaddr() { + use tor_interface::tor_crypto::Ed25519PublicKey; + use libp2p::multiaddr::multiaddr; + let test = V3OnionServiceId::from_public_key(&Ed25519PublicKey::from_raw(&[0; 32]).unwrap()).to_multiaddr(12345); + assert_eq!( + test, + multiaddr!(Onion3(( + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0xCD, 0x0E, 0x03 + ], + 12345 + ))) + ); + assert_eq!( + test.to_string(), + "/onion3/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd:12345" + ); +} + + +impl + Send + Sync + 'static> Transport for TorInterfaceTransport { + type Error = TorInterfaceTransportError; + type Output = OnionStreamStream; + type ListenerUpgrade = std::future::Ready>; + type Dial = BoxFuture<'static, Result>; + + fn listen_on( + &mut self, + id: ListenerId, + onion_address: Multiaddr, + ) -> Result<(), TransportError> { + // If the address is not an onion3 address, return an error + if !matches!(onion_address.into_iter().nth(0), Some(libp2p::multiaddr::Protocol::Onion3(_))) { + return Err(TransportError::MultiaddrNotSupported(onion_address)); + } + + // Find the running onion service that matches the requested address + // If we find it, tag it in [`services`] and insert it into [`listeners`] + let service = self + .services + .iter_mut() + .find(|(service, listener_id)| listener_id.is_none() && service.address().to_multiaddr() == onion_address); + let Some((service, listener_id)) = service + else { + return Err(TransportError::MultiaddrNotSupported(onion_address)); + }; + + + let listener = service.try_clone_inner().and_then(TcpListener::from_std).map_err(TorInterfaceTransportError::Io).map_err(TransportError::Other)?; + *listener_id = Some(id); + + self.listeners.insert(id, listener); + self.waiting_to_announce.insert(id, service.address().clone()); + + Ok(()) + } + + fn remove_listener(&mut self, id: ListenerId) -> bool { + // Take the listener out of the map. This will stop listening on onion service for libp2p connections (we will not poll it anymore) + // However, we will not stop the onion service itself because we might want to reuse it later + // The onion service will be stopped when the transport is dropped + if let Some(_) = self.listeners.remove(&id) { + let Some((_, listener_id)) = self.services.iter_mut().find(|(_, listener_id)| *listener_id == Some(id)) + else { unreachable!() }; + *listener_id = None; + self.waiting_to_announce.remove(&id); + return true; + } + + false + } + + fn dial(&mut self, addr: Multiaddr) -> Result> { + let maybe_tor_addr = match self.conversion_mode { + AddressConversion::DnsOnly => safe_extract(&addr), + AddressConversion::IpAndDns => dangerous_extract(&addr), + }; + + let Some(tor_address) = maybe_tor_addr + else { return Err(TransportError::MultiaddrNotSupported(addr)); }; + let provider = self.provider.clone(); + let circuit = self.circuit; + + Ok(Box::pin(async move { + let stream = lock(&provider).connect(tor_address, circuit).map_err(Self::Error::Client)?; + + tracing::debug!(%addr, "Established connection to peer through Tor"); + + OnionStreamStream::from_onion_stream(stream).map_err(Self::Error::Io) + })) + } + + fn dial_as_listener( + &mut self, + addr: Multiaddr, + ) -> Result> { + self.dial(addr) + } + + fn address_translation(&self, _: &Multiaddr, _: &Multiaddr) -> Option { + None + } + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + while !&self.event_backlog.is_empty() { + match self.event_backlog.swap_remove(0) { + tor_provider::TorEvent::BootstrapStatus { .. } => {} + tor_provider::TorEvent::BootstrapComplete => tracing::debug!("Tor bootstrap complete"), + tor_provider::TorEvent::LogReceived { line } => tracing::debug!(%line), + tor_provider::TorEvent::OnionServicePublished { service_id } => { self.published_services.insert(service_id); }, + } + } + + // This is HashMap::extract_if() but that's unstable rn; not perf-sensitive (self.waiting_to_announce.len() is almost always 0) + if let Some(listener_id) = self.waiting_to_announce.iter().find(|(_, addr)| { + let OnionAddr::V3(addr) = addr; + self.published_services.contains(addr.service_id()) + }).map(|(listener_id, _)| listener_id).copied() { + return Poll::Ready(TransportEvent::NewAddress { + listener_id, + listen_addr: self.waiting_to_announce.remove(&listener_id).unwrap(/*key from find()*/).to_multiaddr(), + }); + } + + let new_events = lock(&self.provider).update().unwrap_or(vec![]); + self.event_backlog.extend(new_events); + if !self.event_backlog.is_empty() { + return self.poll(cx); + } + + for (&listener_id, listener) in &mut self.listeners { + match listener.poll_accept(cx) { + Poll::Ready(Ok((caller, _))) => { + let service_addr = self.services.iter().find(|(_, li)| *li == Some(listener_id)).map(|(ol, _)| ol.address()); + let multi = service_addr.map(|ra| ra.to_multiaddr()).unwrap_or(Multiaddr::empty()); + + return Poll::Ready(TransportEvent::Incoming { + listener_id, + upgrade: std::future::ready(Ok((caller, service_addr.cloned()).into())), + local_addr: multi.clone(), + send_back_addr: multi, + }); + } + + Poll::Ready(Err(err)) => { + return Poll::Ready(TransportEvent::ListenerError { listener_id, error: err.into() }); + } + + Poll::Pending => {}, + } + } + + Poll::Pending + } +} diff --git a/libp2p-tor/src/tor/provider.rs b/libp2p-tor/src/tor/provider.rs new file mode 100644 index 000000000..bdab38af0 --- /dev/null +++ b/libp2p-tor/src/tor/provider.rs @@ -0,0 +1,161 @@ +// Copyright 2022 Hannes Furmans +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! The UnixStream implementation is backported and open-coded from +//! It should be removed when libp2p is updated + +use futures::{AsyncRead, AsyncWrite}; +use tor_interface::tor_provider::{OnionAddr, OnionStream, TargetAddr, TcpOrUnixOnionStream, TcpOrUnixStream}; +use libp2p::tcp::tokio::TcpStream; +#[cfg(unix)] +// use libp2p::unix_stream::tokio::UnixStream; +use tokio::net::UnixStream; + +#[derive(Debug)] +enum TcpOrUnix { + Tcp(TcpStream), + #[cfg(unix)] + Unix(UnixStream), +} + +#[derive(Debug)] +pub struct OnionStreamStream { + pub local_addr: Option, + pub peer_addr: Option, + stream: TcpOrUnix, +} + +impl From<(tokio::net::TcpStream, Option)> for OnionStreamStream { + fn from((stream, local_addr): (tokio::net::TcpStream, Option)) -> Self { + let stream = TcpOrUnix::Tcp(TcpStream(stream)); + Self { local_addr, peer_addr: None, stream } + } +} + +impl OnionStreamStream { + pub fn from_onion_stream(inner: TcpOrUnixOnionStream) -> std::io::Result { + let local_addr = inner.local_addr(); + let peer_addr = inner.peer_addr(); + inner.set_nonblocking(true)?; + let stream = match inner.into() { + TcpOrUnixStream::Tcp(sock) => TcpOrUnix::Tcp(TcpStream(tokio::net::TcpStream::from_std(sock)?)), + #[cfg(unix)] + TcpOrUnixStream::Unix(sock) => TcpOrUnix::Unix(UnixStream::from_std(sock.into())?), + }; + Ok(Self { local_addr, peer_addr, stream }) + } +} + +impl AsyncRead for OnionStreamStream { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut [u8], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(sock) => AsyncRead::poll_read(std::pin::Pin::new(sock), cx, buf), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncRead::poll_read(std::pin::Pin::new(&mut sock), cx, buf), + TcpOrUnix::Unix(sock) => { + let mut read_buf = tokio::io::ReadBuf::new(buf); + futures::ready!(tokio::io::AsyncRead::poll_read(std::pin::Pin::new(sock), cx, &mut read_buf))?; + std::task::Poll::Ready(Ok(read_buf.filled().len())) + } + } + } + + fn poll_read_vectored( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &mut [std::io::IoSliceMut<'_>], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncRead::poll_read_vectored(std::pin::Pin::new(sock), cx, bufs), + #[cfg(unix)] + // TcpOrUnix::Unix(ref mut sock) => AsyncRead::poll_read_vectored(std::pin::Pin::new(sock), cx, bufs), + TcpOrUnix::Unix(_) => { + // From default impl + for b in bufs { + if !b.is_empty() { + return self.poll_read(cx, b); + } + } + + self.poll_read(cx, &mut []) + } + } + } +} + +impl AsyncWrite for OnionStreamStream { + #[inline] + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_write(std::pin::Pin::new(sock), cx, buf), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_write(std::pin::Pin::new(sock), cx, buf), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_write(std::pin::Pin::new(sock), cx, buf) + } + } + + #[inline] + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_flush(std::pin::Pin::new(sock), cx), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_flush(std::pin::Pin::new(sock), cx), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_flush(std::pin::Pin::new(sock), cx) + } + } + + #[inline] + fn poll_close( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_close(std::pin::Pin::new(sock), cx), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_close(std::pin::Pin::new(sock), cx), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_shutdown(std::pin::Pin::new(sock), cx) + } + } + + #[inline] + fn poll_write_vectored( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> std::task::Poll> { + match &mut self.stream { + TcpOrUnix::Tcp(ref mut sock) => AsyncWrite::poll_write_vectored(std::pin::Pin::new(sock), cx, bufs), + #[cfg(unix)] + // TcpOrUnix::Unix(sock) => AsyncWrite::poll_write_vectored(std::pin::Pin::new(sock), cx, bufs), + TcpOrUnix::Unix(ref mut sock) => tokio::io::AsyncWrite::poll_write_vectored(std::pin::Pin::new(sock), cx, bufs) + } + } +} diff --git a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx index 230bf5d7f..05b4b867c 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsBox.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsBox.tsx @@ -62,6 +62,7 @@ import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; import { isValidMultiAddressWithPeerId } from "utils/parseUtils"; import { getNodeStatus } from "renderer/rpc"; import { setStatus } from "store/features/nodesSlice"; +import { getTorForced } from "../../../rpc"; const PLACEHOLDER_ELECTRUM_RPC_URL = "ssl://blockstream.info:700"; const PLACEHOLDER_MONERO_NODE_URL = "http://xmr-node.cakewallet.com:18081"; @@ -694,24 +695,25 @@ function NodeTable({ ); } +const torForced = await getTorForced(); export function TorSettings() { const dispatch = useAppDispatch(); const torEnabled = useSettings((settings) => settings.enableTor); const handleChange = (event: React.ChangeEvent) => dispatch(setTorEnabled(event.target.checked)); - const status = (state: boolean) => (state === true ? "enabled" : "disabled"); return ( - + ); diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index e9fe8eff2..26b7d046f 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -280,6 +280,11 @@ export async function checkContextAvailability(): Promise { return available; } +export async function getTorForced(): Promise { + const forced = await invokeNoArgs("get_tor_forced"); + return forced; +} + export async function getLogsOfSwap( swapId: string, redact: boolean, diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 71bd04ba5..0facb23a4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ rustls = { version = "0.23.26", default-features = false, features = ["ring"] } serde = { workspace = true } serde_json = { workspace = true } swap = { path = "../swap", features = [ "tauri" ] } +swap-env = { path = "../swap-env" } tauri = { version = "^2.0.0", features = [ "config-json5" ] } tauri-plugin-clipboard-manager = "^2.0.0" tauri-plugin-dialog = "2.2.2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4cd8c3947..812a99f73 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ use swap::cli::{ }, command::Bitcoin, }; +use swap_env::env::may_init_tor; use tauri::{async_runtime::RwLock, Manager, RunEvent}; use tauri_plugin_dialog::DialogExt; use zip::{write::SimpleFileOptions, ZipWriter}; @@ -180,6 +181,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ get_balance, get_monero_addresses, + get_tor_forced, get_swap_info, get_swap_infos_all, withdraw_btc, @@ -276,6 +278,11 @@ async fn is_context_available(state: tauri::State<'_, State>) -> Result>) -> Result { + Ok(!may_init_tor()) +} + #[tauri::command] async fn check_monero_node( args: CheckMoneroNodeArgs, diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index f9d3e2fea..477b82c63 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -39,7 +39,7 @@ use swap::protocol::{Database, State}; use swap::seed::Seed; use swap::{bitcoin, monero}; use swap_env::config::{ - initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized, + may_init_tor, initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized, }; use swap_feed; use tracing_subscriber::filter::LevelFilter; @@ -218,9 +218,13 @@ pub async fn main() -> Result<()> { let namespace = XmrBtcNamespace::from_is_testnet(testnet); // Initialize and bootstrap Tor client - let tor_client = create_tor_client(&config.data.dir).await?; - bootstrap_tor_client(tor_client.clone(), None).await?; - let tor_client = tor_client.into(); + let tor_client = if may_init_tor() { + let tor_client = create_tor_client(&config.data.dir).await?; + bootstrap_tor_client(tor_client.clone(), None).await?; + Some(tor_client.into()) + } else { + None + }; let (mut swarm, onion_addresses) = swarm::asb( &seed, @@ -232,11 +236,12 @@ pub async fn main() -> Result<()> { namespace, &rendezvous_addrs, tor_client, + &config.data.dir, config.tor.register_hidden_service, config.tor.hidden_service_num_intro_points, )?; - for listen in config.network.listen.clone() { + for listen in &config.network.listen { if let Err(e) = Swarm::listen_on(&mut swarm, listen.clone()) { tracing::warn!("Failed to listen on network interface {}: {}. Consider removing it from the config.", listen, e); } diff --git a/swap-env/src/env.rs b/swap-env/src/env.rs index 6158274ce..1c00eaf3b 100644 --- a/swap-env/src/env.rs +++ b/swap-env/src/env.rs @@ -1,6 +1,7 @@ use crate::config::Config as AsbConfig; use serde::Serialize; use std::cmp::max; +use std::fs; use std::time::Duration; use time::ext::NumericalStdDuration; @@ -136,6 +137,18 @@ pub fn new(is_testnet: bool, asb_config: &AsbConfig) -> Config { } } +pub fn is_whonix() -> bool { + fs::exists("/usr/share/whonix/marker").unwrap_or(false) +} + +pub fn may_init_tor() -> bool { + let is_whonix = is_whonix(); + if is_whonix { + tracing::info!("On whonix, not starting Tor"); + } + !is_whonix +} + #[cfg(test)] mod tests { use super::*; diff --git a/swap-env/src/prompt.rs b/swap-env/src/prompt.rs index 38b95df6f..1297fdd67 100644 --- a/swap-env/src/prompt.rs +++ b/swap-env/src/prompt.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use crate::defaults::{ default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, }; +use crate::env::may_init_tor; use anyhow::{bail, Context, Result}; use dialoguer::Confirm; use dialoguer::{theme::ColorfulTheme, Input, Select}; @@ -36,6 +37,10 @@ pub fn bitcoin_confirmation_target(default_target: u16) -> Result { /// Prompt user for listen addresses pub fn listen_addresses(default_listen_address: &Multiaddr) -> Result> { + if !may_init_tor() { + return Ok(vec![]); + } + let listen_addresses = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default") .default(format!("{}", default_listen_address)) @@ -122,6 +127,10 @@ pub fn monero_daemon_url() -> Result> { /// Prompt user for Tor hidden service registration pub fn tor_hidden_service() -> Result { + if !may_init_tor() { + return Ok(true); + } + println!("Your ASB needs to be reachable from the outside world to provide quotes to takers."); println!( "Your ASB can run a hidden service for itself. It'll be reachable at an .onion address." diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 8c9852381..8ac15a05b 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -46,7 +46,7 @@ futures = { workspace = true } hex = { workspace = true } jsonrpsee = { workspace = true, features = ["server"] } libp2p = { workspace = true, features = ["tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa"] } -libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service"] } +libp2p-tor = { path = "../libp2p-tor", features = ["listen-onion-service", "legacy-tor-provider"] } moka = { version = "0.12", features = ["sync", "future"] } monero = { workspace = true } monero-rpc = { path = "../monero-rpc" } diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index 16c62b42a..ee6580664 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -22,12 +22,16 @@ use swap_feed::LatestRate; use uuid::Uuid; pub mod transport { - use std::sync::Arc; + use std::sync::{Arc, Mutex}; + use std::path::Path; + use std::fs; + use std::io::Write; use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient}; - use libp2p::{core::transport::OptionalTransport, dns, identity, tcp, Transport}; - use libp2p_tor::AddressConversion; + use libp2p::{core::transport::{OptionalTransport, OrTransport}, dns, identity, tcp, Transport}; + use libp2p_tor::{AddressConversion, tor_interface}; use tor_rtcompat::tokio::TokioRustlsRuntime; + use crate::common::tor::existing_tor_config; use super::*; @@ -36,6 +40,16 @@ pub mod transport { type OnionTransportWithAddresses = (Boxed<(PeerId, StreamMuxerBox)>, Vec); + fn mode600(m: &mut fs::OpenOptions) -> &mut fs::OpenOptions { + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + m.mode(0o600) + } + #[cfg(not(unix))] + m + } + /// Creates the libp2p transport for the ASB. /// /// If you pass in a `None` for `maybe_tor_client`, the ASB will not use Tor at all. @@ -46,12 +60,54 @@ pub mod transport { pub fn new( identity: &identity::Keypair, maybe_tor_client: Option>>, + config_data_dir: &Path, register_hidden_service: bool, num_intro_points: u8, ) -> Result { - let (maybe_tor_transport, onion_addresses) = if let Some(tor_client) = maybe_tor_client { + let (yesmaybe, maybe_tor_transport, onion_addresses) = if let Some((reuse_config, bindaddr)) = existing_tor_config() { + let client = tor_interface::legacy_tor_client::LegacyTorClient::new(reuse_config)?; + let mut tor_transport = libp2p_tor::TorInterfaceTransport::from_provider( + AddressConversion::DnsOnly, Arc::new(Mutex::new(client)), None)?; + + let pk_path = config_data_dir.join(ASB_ONION_SERVICE_NICKNAME).with_extension("pk"); + let pk = match fs::read_to_string(&pk_path).ok() + .and_then(|pk| tor_interface::tor_crypto::Ed25519PrivateKey::from_key_blob(pk.lines().next()?).ok()) { + Some(pk) => pk, + None => { + let pk = tor_interface::tor_crypto::Ed25519PrivateKey::generate(); + let _ = mode600(fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true)) + .open(&pk_path) + .and_then(|mut f| f.write_all(pk.to_key_blob().as_bytes()).and_then(|_| f.write_all(b"\n"))); + pk + } + }; + + let addresses = if register_hidden_service { + match tor_transport.add_onion_service(&pk, ASB_ONION_SERVICE_PORT, None, Some(bindaddr)) + { + Ok(addr) => { + tracing::debug!( + %addr, + "Setting up onion service for libp2p to listen on" + ); + vec![addr] + } + Err(err) => { + tracing::warn!(error=%err, "Failed to listen on onion address"); + vec![] + } + } + } else { + vec![] + }; + + (true, OrTransport::new(OptionalTransport::none(), OptionalTransport::some(tor_transport)), addresses) + } else if let Some(tor_client) = maybe_tor_client { let mut tor_transport = - libp2p_tor::TorTransport::from_client(tor_client, AddressConversion::DnsOnly); + libp2p_tor::TorTransport::from_client(tor_client, libp2p_tor::AddressConversion::DnsOnly); let addresses = if register_hidden_service { let onion_service_config = OnionServiceConfigBuilder::default() @@ -82,13 +138,17 @@ pub mod transport { vec![] }; - (OptionalTransport::some(tor_transport), addresses) + (true, OrTransport::new(OptionalTransport::some(tor_transport), OptionalTransport::none()), addresses) } else { - (OptionalTransport::none(), vec![]) + (false, OrTransport::new(OptionalTransport::none(), OptionalTransport::none()), vec![]) }; - let tcp = maybe_tor_transport - .or_transport(tcp::tokio::Transport::new(tcp::Config::new().nodelay(true))); + let tcp = OrTransport::new(maybe_tor_transport, + if yesmaybe { + OptionalTransport::none() + } else { + OptionalTransport::some(tcp::tokio::Transport::new(tcp::Config::new().nodelay(true))) + }); let tcp_with_dns = dns::tokio::Transport::system(tcp)?; Ok(( diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 843eac4a9..32466b9fe 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -17,7 +17,7 @@ use std::fmt; use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::{Arc, Once}; -use swap_env::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; +use swap_env::env::{may_init_tor, Config as EnvConfig, GetConfig, Mainnet, Testnet}; use swap_fs::system_data_dir; use tauri_bindings::{ MoneroNodeConfig, TauriBackgroundProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle, @@ -486,6 +486,11 @@ impl ContextBuilder { }; let bootstrap_tor_client_task = async { + // Don't init a tor client unless we should use it. + if !may_init_tor() { + return Ok(None); + } + // Bootstrap the Tor client if we have one match unbootstrapped_tor_client.clone() { Some(tor_client) => { diff --git a/swap/src/common/tor.rs b/swap/src/common/tor.rs index 7b61b3513..0bb76ae49 100644 --- a/swap/src/common/tor.rs +++ b/swap/src/common/tor.rs @@ -6,8 +6,21 @@ use crate::cli::api::tauri_bindings::{ }; use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error, TorClient}; use futures::StreamExt; +use swap_env::env::is_whonix; use tor_rtcompat::tokio::TokioRustlsRuntime; +pub fn existing_tor_config() -> Option<( + libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig, + std::net::SocketAddr, +)> { + if is_whonix() { + Some((libp2p_tor::tor_interface::legacy_tor_client::LegacyTorClientConfig::system_from_environment().expect("whonix always has $TOR_... set"), + ([0, 0, 0, 0], 9939).into())) + } else { + None + } +} + /// Creates an unbootstrapped Tor client pub async fn create_tor_client( data_dir: &Path, diff --git a/swap/src/network/swarm.rs b/swap/src/network/swarm.rs index 2e40668e1..74ab08665 100644 --- a/swap/src/network/swarm.rs +++ b/swap/src/network/swarm.rs @@ -9,6 +9,7 @@ use libp2p::swarm::NetworkBehaviour; use libp2p::SwarmBuilder; use libp2p::{identity, Multiaddr, Swarm}; use std::fmt::Debug; +use std::path::Path; use std::sync::Arc; use std::time::Duration; use swap_env::env; @@ -25,6 +26,7 @@ pub fn asb( namespace: XmrBtcNamespace, rendezvous_addrs: &[Multiaddr], maybe_tor_client: Option>>, + config_data_dir: &Path, register_hidden_service: bool, num_intro_points: u8, ) -> Result<(Swarm>, Vec)> @@ -57,6 +59,7 @@ where let (transport, onion_addresses) = asb::transport::new( &identity, maybe_tor_client, + config_data_dir, register_hidden_service, num_intro_points, )?; diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 3ef4ce87a..7f9103f56 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -262,6 +262,7 @@ async fn start_alice( XmrBtcNamespace::Testnet, &[], None, + &db_path, false, 1, ) diff --git a/tor-interface/CMakeLists.txt b/tor-interface/CMakeLists.txt new file mode 100644 index 000000000..6d168a6a9 --- /dev/null +++ b/tor-interface/CMakeLists.txt @@ -0,0 +1,214 @@ +set(tor_interface_sources + Cargo.toml + src/arti_client_tor_client.rs + src/arti_process.rs + src/arti_tor_client.rs + src/censorship_circumvention.rs + src/legacy_tor_client.rs + src/legacy_tor_controller.rs + src/legacy_tor_control_stream.rs + src/legacy_tor_process.rs + src/legacy_tor_version.rs + src/lib.rs + src/mock_tor_client.rs + src/proxy.rs + src/tor_crypto.rs + src/tor_provider.rs) + +set(tor_interface_outputs + ${CARGO_TARGET_DIR}/${CARGO_PROFILE}/libtor_interface.d + ${CARGO_TARGET_DIR}/${CARGO_PROFILE}/libtor_interface.rlib) + +# +# tor-interface crate feature flags +# + +set(TOR_INTERFACE_FEATURE_LIST) +if (ENABLE_MOCK_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "mock-tor-provider") +endif() +if (ENABLE_LEGACY_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "legacy-tor-provider") +endif() +if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "arti-client-tor-provider") +endif() +if (ENABLE_ARTI_TOR_PROVIDER) + list(APPEND TOR_INTERFACE_FEATURE_LIST "arti-tor-provider") +endif() + +list(JOIN TOR_INTERFACE_FEATURE_LIST "," TOR_INTERFACE_FEATURES) +if (TOR_INTERFACE_FEATURES) + set(TOR_INTERFACE_FEATURES "--features" "${TOR_INTERFACE_FEATURES}") +endif() + +# +# build target +# +add_custom_command( + DEPENDS ${tor_interface_sources} + OUTPUT ${tor_interface_outputs} + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + +add_custom_target(tor_interface_target + DEPENDS ${tor_interface_outputs}) + +# +# cargo test target +# +if (ENABLE_TESTS) + if (ENABLE_MOCK_TOR_PROVIDER) + add_test(NAME tor_interface_mock_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mock_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mock_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mock_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mock_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mock_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + if (ENABLE_LEGACY_TOR_PROVIDER) + add_test(NAME tor_interface_legacy_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_legacy_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_legacy_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_system_legacy_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_system_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_system_legacy_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_system_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + if (ENABLE_TOR_EXPERT_BUNDLE) + add_test(NAME tor_interface_legacy_pluggable_transport_bootstrap_cargo_test + COMMAND env TEB_PATH=${TEB_PATH} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_legacy_pluggable_transport_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + set_tests_properties(tor_interface_legacy_pluggable_transport_bootstrap_cargo_test PROPERTIES FIXTURES_REQUIRED tor_expert_bundle_target_fixture) + endif() + if (BUILD_EXAMPLES) + set(tor_interface_legacy_tor_provider_listener_example_outputs + ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-listener${CMAKE_EXECUTABLE_SUFFIX}) + add_custom_command( + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/examples/legacy-tor-provider-listener.rs ${tor_interface_sources} + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-listener${CMAKE_EXECUTABLE_SUFFIX} + COMMAND env CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build --example legacy-tor-provider-listener ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + add_custom_target(tor_interface_legacy_tor_provider_listener_example ALL + DEPENDS ${tor_interface_legacy_tor_provider_listener_example_outputs}) + + set(tor_interface_legacy_tor_provider_provider_example_outputs + ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-provider${CMAKE_EXECUTABLE_SUFFIX}) + add_custom_command( + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/examples/legacy-tor-provider-provider.rs ${tor_interface_sources} + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CARGO_PROFILE}/examples/legacy-tor-provider-provider${CMAKE_EXECUTABLE_SUFFIX} + COMMAND env CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} RUSTFLAGS=${RUSTFLAGS} cargo build --example legacy-tor-provider-provider ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + add_custom_target(tor_interface_legacy_tor_provider_provider_example ALL + DEPENDS ${tor_interface_legacy_tor_provider_provider_example_outputs}) + endif() + endif() + + if (ENABLE_ARTI_CLIENT_TOR_PROVIDER) + add_test(NAME tor_interface_arti_client_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_client_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_arti_client_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_client_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_arti_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_client_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + if (ENABLE_ARTI_TOR_PROVIDER) + add_test(NAME tor_interface_arti_bootstrap_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_arti_bootstrap ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + if (ENABLE_LEGACY_TOR_PROVIDER AND ENABLE_ARTI_CLIENT_TOR_PROVIDER) + add_test(NAME tor_interface_mixed_arti_client_legacy_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_client_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mixed_legacy_arti_client_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_client_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mixed_arti_client_legacy_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_client_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_test(NAME tor_interface_mixed_legacy_arti_client_authenticated_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_client_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + + if (ENABLE_LEGACY_TOR_PROVIDER AND ENABLE_ARTI_TOR_PROVIDER) + # add_test(NAME tor_interface_mixed_arti_legacy_onion_service_cargo_test + # COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_legacy_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + # ) + add_test(NAME tor_interface_mixed_legacy_arti_onion_service_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + # add_test(NAME tor_interface_mixed_arti_legacy_authenticated_onion_service_cargo_test + # COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_arti_legacy_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + # ) + # add_test(NAME tor_interface_mixed_legacy_arti_authenticated_onion_service_cargo_test + # COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_mixed_legacy_arti_authenticated_onion_service ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + # WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + # ) + endif() + + # cryptography + add_test(NAME tor_interface_crypto_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_crypto_ ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + + # tor provider utils + add_test(NAME tor_interface_tor_provider_cargo_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test test_tor_provider_ ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + + # catchall + add_test(NAME tor_interface_cargo_test + COMMAND env TEB_PATH=${TEB_PATH} CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo test ${CARGO_FLAGS} ${TOR_INTERFACE_FEATURES} -- --nocapture + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) +endif() + +# +# fuzz target +# +if (ENABLE_FUZZ_TESTS) + add_test(NAME tor_interface_crypto_cargo_fuzz_test + COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} RUSTFLAGS=${RUSTFLAGS} RUST_BACKTRACE=full cargo fuzz run fuzz_crypto -- -max_total_time=${FUZZ_TEST_MAX_TOTAL_TIME} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) +endif() diff --git a/tor-interface/Cargo.toml b/tor-interface/Cargo.toml new file mode 100644 index 000000000..1ea368ce9 --- /dev/null +++ b/tor-interface/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "tor-interface" +authors = ["morgan ", "Richard Pospesel "] +version = "0.5.0" +rust-version = "1.70" +edition = "2021" +license = "BSD-3-Clause" +description = "A library providing a Rust interface to interact with the legacy tor daemon" +keywords = ["tor", "anonymity"] +repository = "https://github.com/blueprint-freespeech/gosling" + +[dependencies] +arti-client = { workspace = true, features = ["ephemeral-keystore", "experimental-api", "keymgr", "onion-service-client", "onion-service-service", "tokio"], optional = true} +arti-rpc-client-core = { version = "0.32.0", optional = true } +curve25519-dalek = "4.1" +data-encoding = "2.0" +data-encoding-macro = "0.1" +domain = "<= 0.10.0" +idna = "1" +rand = "0.9" +rand_core = "0.9" +regex = "1.9" +sha1 = "0.10" +sha3 = "0.10" +signature = "1.5" +# socks = "0.3" +# socks = { path = "../../../../../rust-socks" } +socks = { git = "https://github.com/nabijaczleweli/rust-socks" } +static_assertions = "1.1" +thiserror = "1.0" +tokio = { version = "1", features = ["macros"], optional = true } +tokio-stream = { version = "0", optional = true } +tokio-mpmc = { version = "0.2", optional = true } +tor-cell = { workspace = true, optional = true } +tor-config = { version = "0.32.0", optional = true } +tor-hsservice = { workspace = true, optional = true, features = ["restricted-discovery"] } +tor-keymgr = { version = "0.32.0", optional = true, features = ["keymgr"] } +tor-llcrypto = { version = "0.32.0", features = ["relay"] } +tor-proto = { workspace = true, features = ["stream-ctrl"], optional = true } +tor-rtcompat = { workspace = true, optional = true } +hmac = { version = "0.12", optional = true } +sha2 = { version = "0.10", optional = true } +zeroize = { version = "1.8", optional = true, features = ["derive"] } + +[dev-dependencies] +anyhow = "1.0" +serial_test = "0.9" +which = "4.4" + +[features] +arti-client-tor-provider = ["arti-client", "tokio", "tokio-stream", "tokio-mpmc", "tor-cell", "tor-config", "tor-hsservice", "tor-keymgr", "tor-proto", "tor-rtcompat"] +arti-tor-provider = ["arti-rpc-client-core"] +mock-tor-provider = [] +legacy-tor-provider = ["hmac", "sha2", "zeroize"] diff --git a/tor-interface/README.md b/tor-interface/README.md new file mode 100644 index 000000000..abdb38c54 --- /dev/null +++ b/tor-interface/README.md @@ -0,0 +1,60 @@ +# Tor-Interface + +Developer-friendly crate providing connectivity to the [Tor Network](https://en.wikipedia.org/wiki/Tor_(network)) and functionality for interacting with Tor-specific cryptographic types. + +This crate is *not* meant to be a general purpose Tor Controller nor does it aim to expose all of the functionality of the underlying Tor implementations. This crate also does not implement any of the Tor Network functionality itself, instead wrapping lower-level implementations. + +## Overview + +The `tor-interface` crate provides the `TorProvider` trait with 3 concrete implementations: + +- ArtiClientTorClient: an experimental wrapper around the [`arti-client`](https://crates.io/crates/arti-client) crate; enabled using the **arti-client-tor-provider** feature flag. +- LegacyTorClient: a wrapper around either an owned or system-provided legacy c-tor daemon (aka 'little-t tor') with some basic configuration options; enabled using the **legacy-tor-provider** feature flag. +- MockTorClient: an in-process, mock implementation which makes no actual connections outside of localhost; enabled with the **mock-tor-provider** feature flag. + +The `TorProvider` trait defines methods for connecting to various types of target addresses (ip, domains, and onion-services) and for creating onion-services. + +## ⚠ Warning ⚠ + +The **arti-client-tor-provider** feature is experimental is not fully implemented. It also depends on the [`arti-client`](https://crates.io/crates/arti-client) crate which is still under active development and is generally not yet ready for production use. + +## Usage + +The following code snippet creates a `LegacyTorClient` which starts a bundled tor daemon, bootstraps, and attempts to connect to [www.example.com](www.example.com). + +```rust,ignore +// construct legacy tor client config +let tor_path = std::path::PathBuf::from_str("/usr/bin/tor").unwrap(); +let mut data_path = std::env::temp_dir(); +data_path.push("tor_data"); + +let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, +}; +// create client from config +let mut tor_client = LegacyTorClient::new(tor_config).unwrap(); + +// bootstrap tor +let mut bootstrap_complete = false; +while !bootstrap_complete { + for event in tor_client.update().unwrap().iter() { + match event { + TorEvent::BootstrapComplete => { + bootstrap_complete = true; + }, + _ => {}, + } + } +} + +// connect to example.com +let target_addr = TargetAddr::from_str("www.example.com:80").unwrap(); +let mut stream: OnionStream = tor_client.connect(target_addr, None).unwrap(); +// and convert to a std::net::TcpStream +let stream: TcpStream = stream.into(); +``` diff --git a/tor-interface/examples/legacy-tor-provider-listener.rs b/tor-interface/examples/legacy-tor-provider-listener.rs new file mode 100644 index 000000000..30618608e --- /dev/null +++ b/tor-interface/examples/legacy-tor-provider-listener.rs @@ -0,0 +1,24 @@ +use std::io::Write; +use tor_interface::legacy_tor_client::{LegacyTorClientConfig, LegacyTorClient}; +use tor_interface::tor_crypto::Ed25519PrivateKey; +use tor_interface::tor_provider::{OnionListener, TorProvider}; + +fn main() { + let mut client = LegacyTorClient::new(LegacyTorClientConfig::system_from_environment().expect("No configuration in the environment")).unwrap(); + client.bootstrap().unwrap(); + println!("{:?}", client.update().unwrap()); + + let pk = Ed25519PrivateKey::generate(); + let ol = client.listener(&pk, 80, None, None).unwrap(); + println!("http://{}.onion", tor_interface::tor_crypto::V3OnionServiceId::from_private_key(&pk)); + + loop { + for u in client.update().unwrap() { + println!("{:?}", u); + } + if let Some(mut peer) = ol.accept().unwrap() { + println!("{:?}", &peer); + peer.write_all(format!("HTTP/1.1 200 OK\r\n\r\n{:?}\n", peer).as_bytes()).unwrap(); + } + } +} diff --git a/tor-interface/examples/legacy-tor-provider-provider.rs b/tor-interface/examples/legacy-tor-provider-provider.rs new file mode 100644 index 000000000..a72fe245f --- /dev/null +++ b/tor-interface/examples/legacy-tor-provider-provider.rs @@ -0,0 +1,33 @@ +use std::io::{BufRead, BufReader, Write}; +use std::str::FromStr; +use tor_interface::legacy_tor_client::{LegacyTorClient, LegacyTorClientConfig}; +use tor_interface::tor_provider::{OnionStream, TargetAddr, TorProvider}; + +fn read_headers(os: S) { + for l in BufReader::new(os).lines().map(Result::unwrap) { + if l.is_empty() { + return; + } + println!("{}", l); + } +} + +fn main() { + let mut client = LegacyTorClient::new(LegacyTorClientConfig::system_from_environment().expect("No configuration in the environment")).unwrap(); + client.bootstrap().unwrap(); + println!("{:?}", client.update().unwrap()); + + let mut sess = client.connect(TargetAddr::from_str("cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion:80").unwrap(), None).unwrap(); + dbg!(&sess); + sess.write(b"GET / HTTP/1.1\r\nHost: cebulka7uxchnbpvmqapg5pfos4ngaxglsktzvha7a5rigndghvadeyd.onion\r\nUser-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.1)\r\nAccept: text/html\r\nAccept-Language: en-US, en; q=0.5\r\n\r\n").unwrap(); + sess.flush().unwrap(); + read_headers(sess); + dbg!(client.update().unwrap()); + + let mut sess = client.connect(TargetAddr::from_str("nabijaczleweli.xyz:80").unwrap(), None).unwrap(); + dbg!(&sess); + sess.write(b"GET / HTTP/1.1\r\nHost: nabijaczleweli.xyz\r\nUser-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.1)\r\nAccept: text/html\r\nAccept-Language: en-US, en; q=0.5\r\n\r\n").unwrap(); + sess.flush().unwrap(); + read_headers(sess); + dbg!(client.update().unwrap()); +} diff --git a/tor-interface/fuzz/.gitignore b/tor-interface/fuzz/.gitignore new file mode 100644 index 000000000..c7af9f5ef --- /dev/null +++ b/tor-interface/fuzz/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +corpus +artifacts diff --git a/tor-interface/fuzz/Cargo.toml b/tor-interface/fuzz/Cargo.toml new file mode 100644 index 000000000..84a91cc22 --- /dev/null +++ b/tor-interface/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tor-interface-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } + +[dependencies.tor-interface] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "fuzz_crypto" +path = "fuzz_targets/fuzz_crypto.rs" +test = false +doc = false diff --git a/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs b/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs new file mode 100644 index 000000000..978acdc6a --- /dev/null +++ b/tor-interface/fuzz/fuzz_targets/fuzz_crypto.rs @@ -0,0 +1,160 @@ +#![no_main] + +// tor_interface +use tor_interface::tor_crypto::*; + +// fuzzing +use libfuzzer_sys::fuzz_target; +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::arbitrary::Arbitrary; + +#[derive(Arbitrary, Debug)] +struct CryptoData<'a> { + ed25519_public_raw: [u8; 32], + onion_service_id: &'a str, + x25519_public_raw: [u8; 32], + message_1: &'a [u8], + message_2: &'a [u8], + ed25519_private_raw_1: [u8; 64], + ed25519_private_raw_2: [u8; 64], + x25519_private_raw_1: [u8; 32], + x25519_private_raw_2: [u8; 32], +} + +fuzz_target!(|data: CryptoData| { + + // + // ed25519 tests + // + + // ensure random bytes don't break ed25519public from_raw + let _ = Ed25519PublicKey::from_raw(&data.ed25519_public_raw); + + // ensure random string doesn't break v3onionserviceid from_string + let _ = V3OnionServiceId::from_string(data.onion_service_id); + + // ensure random bytes don't break x25519public from_raw + let _ = X25519PublicKey::from_raw(&data.x25519_public_raw); + + // try to build key from raw binary blob, return early if invalid + if let Ok(ed25519_private_1) = Ed25519PrivateKey::from_raw(&data.ed25519_private_raw_1) { + // ensure key round-trips through keyblob representation + assert_eq!(Ed25519PrivateKey::from_key_blob(ed25519_private_1.to_key_blob().as_ref()).unwrap(), ed25519_private_1); + + // ensure key round-trips through raw bytes representation + match Ed25519PrivateKey::from_raw(&ed25519_private_1.to_bytes()) { + Ok(ed25519_private) => assert_eq!(ed25519_private, ed25519_private_1), + Err(err) => panic!("{:?}", err), + } + + // derive private keys public key + let ed25519_public_1 = Ed25519PublicKey::from_private_key(&ed25519_private_1); + + // compare onion service id derivation from public vs privat ekey + assert_eq!(V3OnionServiceId::from_private_key(&ed25519_private_1), V3OnionServiceId::from_public_key(&ed25519_public_1)); + let onion_service_id_1 = V3OnionServiceId::from_public_key(&ed25519_public_1); + // ensure service id round-trips through string representation + assert_eq!(V3OnionServiceId::from_string(&onion_service_id_1.to_string()).unwrap(), onion_service_id_1); + + // ensure public key round-trips through service id + assert_eq!(ed25519_public_1, Ed25519PublicKey::from_service_id(&V3OnionServiceId::from_public_key(&ed25519_public_1)).unwrap()); + + // ensure key round-trips through raw bytes representation + assert_eq!(ed25519_public_1, Ed25519PublicKey::from_raw(ed25519_public_1.as_bytes()).unwrap()); + + // sign and verify a message + let ed25519_signature_1 = ed25519_private_1.sign_message(data.message_1); + assert!(ed25519_signature_1.verify(data.message_1, &ed25519_public_1)); + // verify signature does not work for unrelated message + if data.message_1 != data.message_2 { + assert!(!ed25519_signature_1.verify(data.message_2, &ed25519_public_1)); + } + + // ensure we can't verfify another key's signature + if data.ed25519_private_raw_1 != data.ed25519_private_raw_2 { + // try to build key from raw binary blob, return early if invalid + if let Ok(ed25519_private_2) = Ed25519PrivateKey::from_raw(&data.ed25519_private_raw_2) { + + // ensure key round-trips through keyblob representation + assert_eq!(Ed25519PrivateKey::from_key_blob(ed25519_private_2.to_key_blob().as_ref()).unwrap(), ed25519_private_2); + + // ensure key round-trips through raw bytes representation + match Ed25519PrivateKey::from_raw(&ed25519_private_2.to_bytes()) { + Ok(ed25519_private) => assert_eq!(ed25519_private, ed25519_private_2), + Err(err) => panic!("{:?}", err), + } + + // derive private key's public key + let ed25519_public_2 = Ed25519PublicKey::from_private_key(&ed25519_private_2); + + // compare onion service id derivation from public vs privat ekey + assert_eq!(V3OnionServiceId::from_private_key(&ed25519_private_2), V3OnionServiceId::from_public_key(&ed25519_public_2)); + let onion_service_id_2 = V3OnionServiceId::from_public_key(&ed25519_public_2); + // ensure service id round-trips through string representation + assert_eq!(V3OnionServiceId::from_string(&onion_service_id_2.to_string()).unwrap(), onion_service_id_2); + + // ensure public key round-trips through service id + assert_eq!(ed25519_public_2, Ed25519PublicKey::from_service_id(&V3OnionServiceId::from_public_key(&ed25519_public_2)).unwrap()); + + // ensure key round-trips through raw bytes representation + assert_eq!(ed25519_public_2, Ed25519PublicKey::from_raw(ed25519_public_2.as_bytes()).unwrap()); + + + // sign and verify a message + let ed25519_signature_2 = ed25519_private_2.sign_message(data.message_2); + assert!(ed25519_signature_2.verify(data.message_2, &ed25519_public_2)); + + // verify signature does not work for unrelated message + if data.message_1 != data.message_2 { + assert!(!ed25519_signature_2.verify(data.message_1, &ed25519_public_2)); + } + + // verify we cannot verify signatures using the wrong public keys + if ed25519_public_1 != ed25519_public_2 { + assert!(!ed25519_signature_1.verify(data.message_1, &ed25519_public_2)); + assert!(!ed25519_signature_2.verify(data.message_2, &ed25519_public_1)); + } + } + } + } + + // + // x25519 tests + // + + if let Ok(x25519_private_1) = X25519PrivateKey::from_raw(&data.x25519_private_raw_1) { + // ensure round-trips through byte representation + assert_eq!(x25519_private_1, X25519PrivateKey::from_raw(&x25519_private_1.to_bytes()).unwrap()); + assert_eq!(data.x25519_private_raw_1, x25519_private_1.to_bytes()); + // ensure round-trips through base64 representation + assert_eq!(x25519_private_1, X25519PrivateKey::from_base64(&x25519_private_1.to_base64()).unwrap()); + + // ensure converts to e25519 without issue + let _ = Ed25519PrivateKey::from_private_x25519(&x25519_private_1).unwrap(); + + let x25519_public_1 = X25519PublicKey::from_private_key(&x25519_private_1); + // ensure round-trips through byte representation + assert_eq!(x25519_public_1, X25519PublicKey::from_raw(x25519_public_1.as_bytes())); + // ensure round-trips through base32 representation + assert_eq!(x25519_public_1, X25519PublicKey::from_base32(&x25519_public_1.to_base32()).unwrap()); + + if let Ok(x25519_private_2) = X25519PrivateKey::from_raw(&data.x25519_private_raw_2) { + // ensure round-trips through byte representation + assert_eq!(x25519_private_2, X25519PrivateKey::from_raw(&x25519_private_2.to_bytes()).unwrap()); + assert_eq!(data.x25519_private_raw_2, x25519_private_2.to_bytes()); + // ensure round-trips through base64 representation + assert_eq!(x25519_private_2, X25519PrivateKey::from_base64(&x25519_private_2.to_base64()).unwrap()); + + // ensure converts to e25519 without issue + let _ = Ed25519PrivateKey::from_private_x25519(&x25519_private_2).unwrap(); + + let x25519_public_2 = X25519PublicKey::from_private_key(&x25519_private_2); + // ensure round-trips through byte representation + assert_eq!(x25519_public_2, X25519PublicKey::from_raw(x25519_public_2.as_bytes())); + // ensure round-trips through base32 representation + assert_eq!(x25519_public_2, X25519PublicKey::from_base32(&x25519_public_2.to_base32()).unwrap()); + } + } + + +}); diff --git a/tor-interface/src/arti_client_tor_client.rs b/tor-interface/src/arti_client_tor_client.rs new file mode 100644 index 000000000..3dbbc1166 --- /dev/null +++ b/tor-interface/src/arti_client_tor_client.rs @@ -0,0 +1,526 @@ +// standard +use std::future::Future; +use std::io::{Read, Write}; +use std::net::SocketAddr; +use std::ops::DerefMut; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll, Waker}; + +//extern +use arti_client::config::{CfgPath, TorClientConfigBuilder}; +use arti_client::{BootstrapBehavior, DangerouslyIntoTorAddr, DataStream, IntoTorAddr, TorClient}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::{pin, runtime}; +use tokio_mpmc as mpmc; +use tokio_stream::StreamExt; +use tor_cell::relaycell::msg::Connected; +use tor_config::ExplicitOrAuto; +use tor_llcrypto::pk::ed25519::ExpandedKeypair; +use tor_hsservice::config::OnionServiceConfigBuilder; +use tor_hsservice::config::restricted_discovery::HsClientNickname; +use tor_hsservice::status::State; +use tor_hsservice::{HsNickname, RunningOnionService, StreamRequest}; +use tor_keymgr::{config::ArtiKeystoreKind, KeystoreSelector}; +use tor_proto::stream::IncomingStreamRequest; +use tor_rtcompat::PreferredRuntime; + +// internal crates +use crate::tor_crypto::*; +use crate::tor_provider; +use crate::tor_provider::*; + +/// [`ArtiClientTorClient`]-specific error type +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("not implemented")] + NotImplemented(), + + #[error("unable to bind TCP listener")] + TcpListenerBindFailed(#[source] std::io::Error), + + #[error("unable to get TCP listener's local address")] + TcpListenerLocalAddrFailed(#[source] std::io::Error), + + #[error("unable to accept connection on TCP Listener")] + TcpListenerAcceptFailed(#[source] std::io::Error), + + #[error("unable to connect to TCP listener")] + TcpStreamConnectFailed(#[source] std::io::Error), + + #[error("unable to convert tokio::TcpStream to std::net::TcpStream")] + TcpStreamIntoFailed(#[source] std::io::Error), + + #[error("arti-client config-builder error: {0}")] + ArtiClientConfigBuilderError(#[source] arti_client::config::ConfigBuildError), + + #[error("arti-client error: {0}")] + ArtiClientError(#[source] arti_client::Error), + + #[error("arti-client tor-addr error: {0}")] + ArtiClientTorAddrError(#[source] arti_client::TorAddrError), + + #[error("arti-client onion-service startup error: {0}")] + ArtiClientOnionServiceLaunchError(#[source] arti_client::Error), + + #[error("tor-keymgr error: {0}")] + TorKeyMgrError(#[source] tor_keymgr::Error), + + #[error("onion-service config-builder error: {0}")] + OnionServiceConfigBuilderError(#[source] tor_config::ConfigBuildError), +} + +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + +/// The `ArtiClientTorClient` is an in-process [`arti-client`](https://crates.io/crates/arti-client)-based [`TorProvider`]. +/// +/// +pub struct ArtiClientTorClient { + tokio_runtime: Arc, + arti_client: TorClient, + pending_events: Arc>>, + bootstrapped: Arc, +} + +impl ArtiClientTorClient { + /// Construct a new `ArtiClientTorClient` which uses a [Tokio](https://crates.io/crates/tokio) runtime internally for all async operations. + pub fn new( + tokio_runtime: Arc, + root_data_directory: &Path, + ) -> Result { + // set custom config options + let mut config_builder: TorClientConfigBuilder = Default::default(); + + // manually set arti cache and data directories so we can have + // multiple concurrent instances and control where it writes + let mut cache_dir = PathBuf::from(root_data_directory); + cache_dir.push("cache"); + config_builder + .storage() + .cache_dir(CfgPath::new_literal(cache_dir)) + .keystore() + .primary().kind(ExplicitOrAuto::Explicit(ArtiKeystoreKind::Ephemeral)); + + let mut state_dir = PathBuf::from(root_data_directory); + state_dir.push("state"); + config_builder + .storage() + .state_dir(CfgPath::new_literal(state_dir)); + + // disable access to clearnet addresses and enable access to onion services + config_builder + .address_filter() + .allow_local_addrs(false) + .allow_onion_addrs(true); + + let config = match config_builder.build() { + Ok(config) => config, + Err(err) => return Err(err).map_err(Error::ArtiClientConfigBuilderError), + }; + + let arti_client = tokio_runtime.block_on(async { + TorClient::builder() + .config(config) + .bootstrap_behavior(BootstrapBehavior::Manual) + .create_unbootstrapped() + .map_err(Error::ArtiClientError) + + // TODO: implement TorEvent::LogReceived events once upstream issue is resolved: + // https://gitlab.torproject.org/tpo/core/arti/-/issues/1356 + })?; + + let pending_events = std::vec![TorEvent::LogReceived { + line: "Starting arti-client TorProvider".to_string() + }]; + let pending_events = Arc::new(Mutex::new(pending_events)); + + Ok(Self { + tokio_runtime, + arti_client, + pending_events, + bootstrapped: Arc::new(AtomicBool::new(false)), + }) + } +} + +impl TorProvider for ArtiClientTorClient { + type Stream = ArtiClientOnionStream; + type Listener = ArtiClientOnionListener; + + fn update(&mut self) -> Result, tor_provider::Error> { + std::thread::sleep(std::time::Duration::from_millis(16)); + match self.pending_events.lock() { + Ok(mut pending_events) => Ok(std::mem::take(pending_events.deref_mut())), + Err(_) => { + unreachable!("another thread panicked while holding this pending_events mutex") + } + } + } + + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { + // save progress events + let mut bootstrap_events = self.arti_client.bootstrap_events(); + let pending_events = self.pending_events.clone(); + let bootstrapped = self.bootstrapped.clone(); + self.tokio_runtime.spawn(async move { + while let Some(evt) = bootstrap_events.next().await { + if bootstrapped.load(Ordering::Relaxed) { + break; + } + match pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapStatus { + progress: (evt.as_frac().clamp(0.0f32, 1.0f32) * 100f32) as u32, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); + // TODO: properly handle evt.blocked() with a new TorEvent::Error or something + } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), + } + } + }); + + // initiate bootstrap + let arti_client = self.arti_client.clone(); + let pending_events = self.pending_events.clone(); + let bootstrapped = self.bootstrapped.clone(); + self.tokio_runtime.spawn(async move { + match arti_client.bootstrap().await { + Ok(()) => match pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapStatus { + progress: 100, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); + pending_events.push(TorEvent::BootstrapComplete); + bootstrapped.store(true, Ordering::Relaxed); + return; + } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), + }, + Err(_err) => { + // TODO: add an error event to TorEvent + } + } + }); + + Ok(()) + } + + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), tor_provider::Error> { + let ed25519_public = Ed25519PublicKey::from_service_id(service_id).unwrap(); + let hs_id = ed25519_public.as_bytes().clone(); + + self.arti_client.insert_service_discovery_key(KeystoreSelector::Primary, hs_id.into(), client_auth.inner().clone().into()).map_err(Error::ArtiClientError)?; + + Ok(()) + } + + fn remove_client_auth( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { + let ed25519_public = Ed25519PublicKey::from_service_id(service_id).unwrap(); + let hs_id = ed25519_public.as_bytes().clone(); + + self.arti_client.remove_service_discovery_key(KeystoreSelector::Primary, hs_id.into()).map_err(Error::ArtiClientError)?; + + Ok(()) + } + + fn connect( + &mut self, + target: TargetAddr, + circuit: Option, + ) -> Result { + // stream isolation not implemented yet + if circuit.is_some() { + return Err(Error::NotImplemented().into()); + } + + // connect to onion service + let arti_target = match target.clone() { + TargetAddr::Socket(socket_addr) => socket_addr.into_tor_addr_dangerously(), + TargetAddr::Domain(domain_addr) => { + (domain_addr.domain(), domain_addr.port()).into_tor_addr() + } + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { + service_id, + virt_port, + })) => (format!("{}.onion", service_id), virt_port).into_tor_addr(), + } + .map_err(Error::ArtiClientTorAddrError)?; + + let arti_client = self.arti_client.clone(); + let data_stream = self + .tokio_runtime + .block_on(async move { arti_client.connect(arti_target).await }) + .map_err(Error::ArtiClientError)?; + + Ok(ArtiClientOnionStream { + tokio_runtime: self.tokio_runtime.clone(), + data_stream, + nonblocking: AtomicBool::new(false), + local_addr: None, + peer_addr: Some(target), + }) + } + + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, + _bind_addr: Option, + ) -> Result { + // generate a nickname to identify this onion service + let service_id = V3OnionServiceId::from_private_key(private_key); + let hs_nickname = match HsNickname::new(service_id.to_string()) { + Ok(nickname) => nickname, + Err(_) => { + panic!("v3 onion service id string representation should be a valid HsNickname") + } + }; + // generate a new HsIdKeypair (from an Ed25519PrivateKey) + // clone() isn't implemented for ExpandedKeypair >:[ + let secret_key_bytes = private_key.inner().to_secret_key_bytes(); + let hs_id_keypair = ExpandedKeypair::from_secret_key_bytes(secret_key_bytes) + .unwrap(); + + // create an OnionServiceConfig with the ephemeral nickname + let mut onion_service_config_builder = OnionServiceConfigBuilder::default(); + onion_service_config_builder + .nickname(hs_nickname); + + // add authorised client keys if they exist + if let Some(authorized_clients) = authorized_clients { + if !authorized_clients.is_empty() { + let restricted_discovery_config = onion_service_config_builder + .restricted_discovery(); + restricted_discovery_config.enabled(true); + + for (i, key) in authorized_clients.iter().enumerate() { + let nickname = format!("client_{i}"); + restricted_discovery_config + .static_keys() + .access() + .push(( + HsClientNickname::from_str(nickname.as_str()).unwrap(), + key.inner().clone().into(), + )); + } + } + } + + let onion_service_config = onion_service_config_builder.build() + .map_err(Error::OnionServiceConfigBuilderError)?; + + let (onion_service, rend_requests) = self.arti_client + .launch_onion_service_with_hsid(onion_service_config, hs_id_keypair.into()) + .map_err(Error::ArtiClientOnionServiceLaunchError)?; + + // start a task to signal onion service published + let pending_events = self.pending_events.clone(); + let mut status_events = onion_service.status_events(); + let service_id_clone = service_id.clone(); + + self.tokio_runtime.spawn(async move { + while let Some(evt) = status_events.next().await { + match evt.state() { + tor_hsservice::status::State::Running => match pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::OnionServicePublished { service_id: service_id_clone }); + return; + } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), + }, + _ => (), + } + } + }); + + let (sender, receiver) = mpmc::channel(1); + self.tokio_runtime.spawn(async move { + let mut stream_requests = tor_hsservice::handle_rend_requests(rend_requests); + while let Some(stream_request) = stream_requests.next().await { + if sender.send(stream_request).await.is_err() { + return; + } + } + }); + + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); + // onion-service is torn down when `onion_service` is dropped + Ok(ArtiClientOnionListener { + tokio_runtime: self.tokio_runtime.clone(), + stream_requests: receiver, + virt_port, + + nonblocking: AtomicBool::new(false), + onion_service, + onion_addr, + }) + } + + fn generate_token(&mut self) -> CircuitToken { + 0usize + } + + fn release_token(&mut self, _token: CircuitToken) {} +} + +#[derive(Debug)] +pub struct ArtiClientOnionStream { + tokio_runtime: Arc, + data_stream: DataStream, + + nonblocking: AtomicBool, + peer_addr: Option, + local_addr: Option, +} + +macro_rules! fwd { + ($self:expr, $func:tt, $($args:expr),*) => {{ + pin! { + let fut = $self.data_stream.$func($($args),*); + } + if $self.nonblocking.load(Ordering::Relaxed) { + match fut.poll(&mut Context::from_waker(Waker::noop())) { + Poll::Ready(ret) => ret, + Poll::Pending => Err(std::io::Error::new(std::io::ErrorKind::WouldBlock, "")), + } + } else { + $self.tokio_runtime.block_on(fut) + } + }} +} + +impl Read for ArtiClientOnionStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + fwd!(self, read, buf) + } +} + +impl Write for ArtiClientOnionStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + fwd!(self, write, buf) + } + fn flush(&mut self) -> std::io::Result<()> { + fwd!(self, flush, ) + } + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + fwd!(self, write_vectored, bufs) + } +} + +impl OnionStream for ArtiClientOnionStream { + fn peer_addr(&self) -> Option { + self.peer_addr.clone() + } + + fn local_addr(&self) -> Option { + self.local_addr.clone() + } + + fn try_clone(&self) -> std::io::Result where Self: Sized { + Err(std::io::Error::new(std::io::ErrorKind::Other, "not available")) + } + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.nonblocking.store(nonblocking, Ordering::Relaxed); + Ok(()) + } + + fn into_raw(self) -> OnionStreamIntoRaw { + unimplemented!() + } +} + +pub struct ArtiClientOnionListener { + tokio_runtime: Arc, + stream_requests: mpmc::Receiver, + virt_port: u16, + + nonblocking: AtomicBool, + onion_service: Arc, + onion_addr: OnionAddr, +} + +impl OnionListener for ArtiClientOnionListener { + type Stream = ArtiClientOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.nonblocking.store(nonblocking, Ordering::Relaxed); + Ok(()) + } + + fn accept(&self) -> std::io::Result> { + pin! { + let fut = async { + while let Some(stream_request) = self.stream_requests.recv().await.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? { + let should_accept = + if let IncomingStreamRequest::Begin(begin) = stream_request.request() { + // we only accept connections on the virt port + begin.port() == self.virt_port + } else { + false + }; + + if should_accept { + let data_stream = + match stream_request.accept(Connected::new_empty()).await { + Ok(data_stream) => data_stream, + // TODO: probably not our problem + _ => continue, + }; + + return Ok(Some(ArtiClientOnionStream { + tokio_runtime: self.tokio_runtime.clone(), + data_stream, + nonblocking: AtomicBool::new(false), + local_addr: Some(self.onion_addr.clone()), + peer_addr: None, + })); + } else { + // either requesting the wrong port or the wrong type of stream request + let _ = stream_request.shutdown_circuit(); + } + } + match self.onion_service.status().state() { + s @ State::Shutdown | s @ State::DegradedUnreachable | s @ State::Broken => Err(std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", s))), + _ => Ok(None), + } + }; + } + if self.nonblocking.load(Ordering::Relaxed) { + match fut.poll(&mut Context::from_waker(Waker::noop())) { + Poll::Ready(ret) => ret, + Poll::Pending => Ok(None), + } + } else { + self.tokio_runtime.block_on(fut) + } + } + + fn address(&self) -> &OnionAddr { + &self.onion_addr + } +} diff --git a/tor-interface/src/arti_process.rs b/tor-interface/src/arti_process.rs new file mode 100644 index 000000000..a4ce8489c --- /dev/null +++ b/tor-interface/src/arti_process.rs @@ -0,0 +1,229 @@ +// standard +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; +use std::fs::File; +use std::io::{BufRead, BufReader, Write}; +use std::ops::Drop; +use std::process::{Child, ChildStdout, Command, Stdio}; +use std::path::Path; +use std::sync::{Mutex, Weak}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("provided arti bin path '{0}' must be an absolute path")] + ArtiBinPathNotAbsolute(String), + + #[error("provided data directory '{0}' must be an absolute path")] + ArtiDataDirectoryPathNotAbsolute(String), + + #[error("failed to create data directory: {0}")] + ArtiDataDirectoryCreationFailed(#[source] std::io::Error), + + #[error("file exists in provided data directory path '{0}'")] + ArtiDataDirectoryPathExistsAsFile(String), + + #[error("unable to set permissions for data directory: {0}")] + ArtiDataDirectorySetPermissionsFailed(#[source] std::io::Error), + + #[error("failed to create arti.toml file: {0}")] + ArtiTomlFileCreationFailed(#[source] std::io::Error), + + #[error("failed to write arti.toml file: {0}")] + ArtiTomlFileWriteFailed(#[source] std::io::Error), + + #[error("failed to create rpc.toml file: {0}")] + RpcTomlFileCreationFailed(#[source] std::io::Error), + + #[error("failed to write rpc.toml file: {0}")] + RpcTomlFileWriteFailed(#[source] std::io::Error), + + #[error("failed to start arti process: {0}")] + ArtiProcessStartFailed(#[source] std::io::Error), + + #[error("unable to take arti process stdout")] + ArtiProcessStdoutTakeFailed(), + + #[error("failed to spawn arti process stdout read thread: {0}")] + ArtiStdoutReadThreadSpawnFailed(#[source] std::io::Error), +} + +pub(crate) struct ArtiProcess { + process: Child, + connect_string: String, +} + +impl ArtiProcess { + pub fn new(arti_bin_path: &Path, data_directory: &Path, stdout_lines: Weak>>) -> Result { + // verify provided paths are absolute + if arti_bin_path.is_relative() { + return Err(Error::ArtiBinPathNotAbsolute(format!( + "{}", + arti_bin_path.display() + ))); + } + if data_directory.is_relative() { + return Err(Error::ArtiDataDirectoryPathNotAbsolute(format!( + "{}", + data_directory.display() + ))); + } + + // create data directory if it doesn't exist + if !data_directory.exists() { + fs::create_dir_all(data_directory).map_err(Error::ArtiDataDirectoryCreationFailed)?; + } else if data_directory.is_file() { + return Err(Error::ArtiDataDirectoryPathExistsAsFile(format!( + "{}", + data_directory.display() + ))); + } + + // arti data directory must not be world-writable on unix platforms when using a unix domain socket endpoint + if cfg!(unix) { + let perms = PermissionsExt::from_mode(0o700); + fs::set_permissions(data_directory, perms).map_err(Error::ArtiDataDirectorySetPermissionsFailed)?; + } + + // construct paths to arti files file + let arti_toml = data_directory.join("arti.toml").display().to_string(); + let cache_dir = data_directory.join("cache").display().to_string(); + let state_dir = data_directory.join("state").display().to_string(); + + let mut arti_toml_content = format!("\ + [rpc]\n\ + enable = true\n\n\ + [rpc.listen.user-default]\n\ + enable = false\n\n\ + [rpc.listen.system-default]\n\ + enable = false\n\n\ + [storage]\n\ + cache_dir = \"{cache_dir}\"\n\ + state_dir = \"{state_dir}\"\n\n\ + [storage.keystore]\n\ + enabled = true\n\n\ + [storage.keystore.primary]\n\ + kind = \"ephemeral\"\n\n\ + [storage.permissions]\n\ + dangerously_trust_everyone = true\n\n\ + "); + + let connect_string = if cfg!(unix) { + // use domain socket for unix + let unix_rpc_toml_path = data_directory.join("rpc.toml").display().to_string(); + + arti_toml_content.push_str(format!("\ + [rpc.listen.unix-point]\n\ + enable = true\n\ + file = \"{unix_rpc_toml_path}\"\n\n\ + ").as_str()); + + let socket_path = data_directory.join("rpc.socket").display().to_string(); + + let unix_rpc_toml_content = format!("\ + [connect]\n\ + socket = \"unix:{socket_path}\"\n\ + auth = \"none\"\n\ + "); + + let mut unix_rpc_toml_file = + File::create(&unix_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?; + unix_rpc_toml_file + .write_all(unix_rpc_toml_content.as_bytes()) + .map_err(Error::RpcTomlFileWriteFailed)?; + + unix_rpc_toml_path + } else { + // use tcp socket everywhere else + let tcp_rpc_toml_path = data_directory.join("rpc.toml").display().to_string(); + + arti_toml_content.push_str(format!("\ + [rpc.listen.tcp-point]\n\ + enable = true\n\ + file = \"{tcp_rpc_toml_path}\"\n\n\ + ").as_str()); + + let cookie_path = data_directory.join("rpc.cookie").display().to_string(); + + const RPC_PORT: u16 = 18929; + + let tcp_rpc_toml_content = format!("\ + [connect]\n\ + socket = \"inet:127.0.0.1:{RPC_PORT}\"\n\ + auth = {{ cookie = {{ path = \"{cookie_path}\" }} }}\n\ + "); + + let mut tcp_rpc_toml_file = + File::create(&tcp_rpc_toml_path).map_err(Error::RpcTomlFileCreationFailed)?; + tcp_rpc_toml_file + .write_all(tcp_rpc_toml_content.as_bytes()) + .map_err(Error::RpcTomlFileWriteFailed)?; + + tcp_rpc_toml_path + }; + + let mut arti_toml_file = + File::create(&arti_toml).map_err(Error::ArtiTomlFileCreationFailed)?; + arti_toml_file + .write_all(arti_toml_content.as_bytes()) + .map_err(Error::ArtiTomlFileWriteFailed)?; + + let mut process = Command::new(arti_bin_path.as_os_str()) + .stdout(Stdio::piped()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // set working directory to data directory + .current_dir(data_directory) + // proxy subcommand + .arg("proxy") + // point to our above written arti.toml file + .arg("--config") + .arg(arti_toml) + .spawn() + .map_err(Error::ArtiProcessStartFailed)?; + + // spawn a task to read stdout lines and forward to list + let stdout = BufReader::new(match process.stdout.take() { + Some(stdout) => stdout, + None => return Err(Error::ArtiProcessStdoutTakeFailed()), + }); + std::thread::Builder::new() + .name("arti_stdout_reader".to_string()) + .spawn(move || { + ArtiProcess::read_stdout_task(&stdout_lines, stdout); + }) + .map_err(Error::ArtiStdoutReadThreadSpawnFailed)?; + + Ok(ArtiProcess { process, connect_string }) + } + + pub fn connect_string(&self) -> &str { + self.connect_string.as_str() + } + + fn read_stdout_task( + stdout_lines: &std::sync::Weak>>, + mut stdout: BufReader, + ) { + while let Some(stdout_lines) = stdout_lines.upgrade() { + let mut line = String::default(); + // read line + if stdout.read_line(&mut line).is_ok() { + // remove trailing '\n' + line.pop(); + // then acquire the lock on the line buffer + let mut stdout_lines = match stdout_lines.lock() { + Ok(stdout_lines) => stdout_lines, + Err(_) => unreachable!(), + }; + stdout_lines.push(line); + } + } + } +} + +impl Drop for ArtiProcess { + fn drop(&mut self) { + let _ = self.process.kill(); + } +} diff --git a/tor-interface/src/arti_tor_client.rs b/tor-interface/src/arti_tor_client.rs new file mode 100644 index 000000000..361736cd0 --- /dev/null +++ b/tor-interface/src/arti_tor_client.rs @@ -0,0 +1,261 @@ +// std +use std::collections::BTreeMap; +use std::ops::DerefMut; +use std::path::PathBuf; +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +// extern +use arti_rpc_client_core::{RpcConn, RpcConnBuilder}; + +// internal crates +use crate::tor_crypto::*; +use crate::tor_provider; +use crate::tor_provider::*; +use crate::arti_process::*; + +/// [`ArtiTorClient`]-specific error type +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("failed to create ArtiProcess object: {0}")] + ArtiProcessCreationFailed(#[source] crate::arti_process::Error), + + #[error("failed to connect to ArtiProcess after {0:?}")] + ArtiRpcConnectFailed(std::time::Duration), + + #[error("arti not bootstrapped")] + ArtiNotBootstrapped(), + + #[error("failed to connect: {0}")] + ArtiOpenStreamFailed(#[source] arti_rpc_client_core::StreamError), + + #[error("invalid circuit token: {0}")] + CircuitTokenInvalid(CircuitToken), + + #[error("not implemented")] + NotImplemented(), +} + +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + +#[derive(Clone, Debug)] +pub enum ArtiTorClientConfig { + BundledArti { + arti_bin_path: PathBuf, + data_directory: PathBuf, + }, + SystemArti { + + }, +} + +pub struct ArtiTorClient { + _daemon: Option, + rpc_conn: RpcConn, + pending_log_lines: Arc>>, + pending_events: Arc>>, + bootstrapped: bool, + // our list of circuit tokens for the arti daemon + circuit_token_counter: usize, + circuit_tokens: BTreeMap, +} + +impl ArtiTorClient { + pub fn new(config: ArtiTorClientConfig) -> Result { + let pending_log_lines: Arc>> = Default::default(); + + let (daemon, rpc_conn) = match &config { + ArtiTorClientConfig::BundledArti { + arti_bin_path, + data_directory, + } => { + + // launch arti + let daemon = + ArtiProcess::new(arti_bin_path.as_path(), data_directory.as_path(), Arc::downgrade(&pending_log_lines)) + .map_err(Error::ArtiProcessCreationFailed)?; + + let rpc_conn = { + // try to open an rpc connnection for 5 seconds beore giving up + let timeout = Duration::from_secs(5); + let mut rpc_conn: Option = None; + + let start = Instant::now(); + while rpc_conn.is_none() && start.elapsed() < timeout { + + let mut builder = RpcConnBuilder::new(); + builder.prepend_literal_path(daemon.connect_string().into()); + + rpc_conn = builder.connect().map_or(None, |rpc_conn| Some(rpc_conn)); + } + + if let Some(rpc_conn) = rpc_conn { + rpc_conn + } else { + return Err(Error::ArtiRpcConnectFailed(timeout))? + } + }; + + (daemon, rpc_conn) + }, + _ => { + return Err(Error::NotImplemented().into()) + } + }; + + let pending_events = std::vec![TorEvent::LogReceived { + line: "Starting arti TorProvider".to_string() + }]; + let pending_events = Arc::new(Mutex::new(pending_events)); + + Ok(Self { + _daemon: Some(daemon), + rpc_conn, + pending_log_lines, + pending_events, + bootstrapped: false, + circuit_token_counter: 0, + circuit_tokens: Default::default(), + }) + } +} + +impl TorProvider for ArtiTorClient { + type Stream = TcpOrUnixOnionStream; + type Listener = TcpOnionListener; + + fn update(&mut self) -> Result, tor_provider::Error> { + std::thread::sleep(std::time::Duration::from_millis(16)); + let mut tor_events = match self.pending_events.lock() { + Ok(mut pending_events) => std::mem::take(pending_events.deref_mut()), + Err(_) => { + unreachable!("another thread panicked while holding this pending_events mutex") + } + }; + // take our log lines + let mut log_lines = match self.pending_log_lines.lock() { + Ok(mut pending_log_lines) => std::mem::take(pending_log_lines.deref_mut()), + Err(_) => { + unreachable!("another thread panicked while holding this pending_log_lines mutex") + } + }; + + // append raw lines as TorEvent + for log_line in log_lines.iter_mut() { + tor_events.push(TorEvent::LogReceived { + line: std::mem::take(log_line), + }); + } + + Ok(tor_events) + } + + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { + // TODO: seems no way to start arti without automatically bootstrapping + if !self.bootstrapped { + match self.pending_events.lock() { + Ok(mut pending_events) => { + pending_events.push(TorEvent::BootstrapStatus { + progress: 0, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); + pending_events.push(TorEvent::BootstrapStatus { + progress: 100, + tag: "no-tag".to_string(), + summary: "no summary".to_string(), + }); + pending_events.push(TorEvent::BootstrapComplete); + } + Err(_) => unreachable!( + "another thread panicked while holding this pending_events mutex" + ), + } + self.bootstrapped = true; + } + Ok(()) + } + + fn add_client_auth( + &mut self, + _service_id: &V3OnionServiceId, + _client_auth: &X25519PrivateKey, + ) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn remove_client_auth( + &mut self, + _service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { + Err(Error::NotImplemented().into()) + } + + fn connect( + &mut self, + target: TargetAddr, + circuit_token: Option, + ) -> Result { + if !self.bootstrapped { + return Err(Error::ArtiNotBootstrapped().into()); + } + + // convert TargetAddr to (String, u16) tuple + let (host, port) = match &target { + TargetAddr::Socket(socket_addr) => (format!("{:?}", socket_addr.ip()), socket_addr.port()), + TargetAddr::OnionService(OnionAddr::V3(onion_addr)) => (format!("{}.onion", onion_addr.service_id()), onion_addr.virt_port()), + TargetAddr::Domain(domain_addr) => (domain_addr.domain().to_string(), domain_addr.port()), + }; + + // map circuit_token to isolation string for arti + let isolation = if let Some(circuit_token) = circuit_token { + if let Some(isolation) = self.circuit_tokens.get(&circuit_token) { + isolation.as_str() + } else { + return Err(Error::CircuitTokenInvalid(circuit_token))?; + } + } else { + "" + }; + + // connect to target + let stream = self.rpc_conn.open_stream(None, (host.as_str(), port), isolation) + .map_err(Error::ArtiOpenStreamFailed)?; + + Ok(TcpOrUnixOnionStream { + stream: stream.into(), + local_addr: None, + peer_addr: Some(target), + }) + } + + fn listener( + &mut self, + _private_key: &Ed25519PrivateKey, + _virt_port: u16, + _authorized_clients: Option<&[X25519PublicKey]>, + _bind_addr: Option, + ) -> Result { + Err(Error::NotImplemented().into()) + } + + fn generate_token(&mut self) -> CircuitToken { + const ISOLATION_TOKEN_LEN: usize = 32; + let new_token = self.circuit_token_counter; + self.circuit_token_counter += 1; + self.circuit_tokens.insert( + new_token, + generate_password(ISOLATION_TOKEN_LEN)); + + new_token + } + + fn release_token(&mut self, token: CircuitToken) { + self.circuit_tokens.remove(&token); + } +} diff --git a/tor-interface/src/censorship_circumvention.rs b/tor-interface/src/censorship_circumvention.rs new file mode 100644 index 000000000..f1f1eb915 --- /dev/null +++ b/tor-interface/src/censorship_circumvention.rs @@ -0,0 +1,277 @@ +// standard +use std::net::SocketAddr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::OnceLock; + +// extern crates +use regex::Regex; + +#[derive(Clone, Debug)] +/// Configuration for a pluggable-transport +pub struct PluggableTransportConfig { + transports: Vec, + path_to_binary: PathBuf, + options: Vec, +} + +#[derive(thiserror::Error, Debug)] +/// Error returned on failure to construct a [`PluggableTransportConfig`] +pub enum PluggableTransportConfigError { + #[error("pluggable transport name '{0}' is invalid")] + /// transport names must be a valid C identifier + TransportNameInvalid(String), + #[error("unable to use '{0}' as pluggable transport binary path, {1}")] + /// configuration only allows aboslute paths to binaries + BinaryPathInvalid(String, String), +} + +// per the PT spec: https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt +static TRANSPORT_PATTERN: OnceLock = OnceLock::new(); +fn init_transport_pattern() -> Regex { + Regex::new(r"(?m)^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap() +} + +/// Configuration struct for a pluggable-transport which conforms to the v1.0 pluggable-transport [specification](https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/main/releases/PTSpecV1.0/pt-1_0.txt) +impl PluggableTransportConfig { + /// Construct a new `PluggableTransportConfig`. Each `transport` string must be a [valid C identifier](https://github.com/Pluggable-Transports/Pluggable-Transports-spec/blob/c92e59a9fa6ba11c181f4c5ec9d533eaa7d9d7f3/releases/PTSpecV1.0/pt-1_0.txt#L144) while `path_to_binary` must be an absolute path. + pub fn new( + transports: Vec, + path_to_binary: PathBuf, + ) -> Result { + let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); + // validate each transport + for transport in &transports { + if !transport_pattern.is_match(&transport) { + return Err(PluggableTransportConfigError::TransportNameInvalid( + transport.clone(), + )); + } + } + + // pluggable transport path must be absolute so we can fix it up for individual + // TorProvider implementations + if !path_to_binary.is_absolute() { + return Err(PluggableTransportConfigError::BinaryPathInvalid( + format!("{:?}", path_to_binary.display()), + "must be an absolute path".to_string(), + )); + } + + Ok(Self { + transports, + path_to_binary, + options: Default::default(), + }) + } + + /// Get a reference to this `PluggableTransportConfig`'s list of transports. + pub fn transports(&self) -> &Vec { + &self.transports + } + + /// Get a reference to this `PluggableTransportConfig`'s `PathBuf` containing the absolute path to the pluggable-transport binary. + pub fn path_to_binary(&self) -> &PathBuf { + &self.path_to_binary + } + + /// Get a reference to this `PluggableTransportConfig`'s list of command-line options + pub fn options(&self) -> &Vec { + &self.options + } + + /// Add a command-line option used to invoke this pluggable-transport. + pub fn add_option(&mut self, arg: String) { + self.options.push(arg); + } +} + +/// Configuration for a bridge line to be used with a pluggable-transport +#[derive(Clone, Debug)] +pub struct BridgeLine { + transport: String, + address: SocketAddr, + fingerprint: String, + keyvalues: Vec<(String, String)>, +} + +#[derive(thiserror::Error, Debug)] +/// Error returned on failure to construct a [`BridgeLine`] +pub enum BridgeLineError { + #[error("bridge line '{0}' missing transport")] + /// Provided bridge line missing transport + TransportMissing(String), + + #[error("bridge line '{0}' missing address")] + /// Provided bridge line missing address + AddressMissing(String), + + #[error("bridge line '{0}' missing fingerprint")] + /// Provided bridge line missing fingerprint + FingerprintMissing(String), + + #[error("transport name '{0}' is invalid")] + /// Invalid transport name (must be a valid C identifier) + TransportNameInvalid(String), + + #[error("address '{0}' cannot be parsed as IP:PORT")] + /// Provided bridge line's address not parseable + AddressParseFailed(String), + + #[error("key=value '{0}' is invalid")] + /// A key/value pair in invalid format + KeyValueInvalid(String), + + #[error("bridge address port must not be 0")] + /// Invalid bridge address port + AddressPortInvalid, + + #[error("fingerprint '{0}' is invalid")] + /// Fingerprint is not parseable (must be length 40 base16 string) + FingerprintInvalid(String), +} + +/// A `BridgeLine` contains the information required to connect to a bridge through the means of a particular pluggable-transport (defined in a `PluggableTransportConfi`). For more information, see: +/// - [https://tb-manual.torproject.org/bridges/](https://tb-manual.torproject.org/bridges/) +impl BridgeLine { + /// Construct a new `BridgeLine` from its constiuent parts. The `transport` argument must be a valid C identifier and must have an associated `transport` defined in an associated `PluggableTransportConfig`. The `address` must have a non-zero port. The `fingerprint` is a length 40 base16-encoded string. Finally, the keys in the `keyvalues` list must not contain space (` `) or equal (`=`) characters. + /// + /// In practice, bridge lines are distributed as entire strings so most consumers of these APIs are not likely to need this particular function. + pub fn new( + transport: String, + address: SocketAddr, + fingerprint: String, + keyvalues: Vec<(String, String)>, + ) -> Result { + let transport_pattern = TRANSPORT_PATTERN.get_or_init(init_transport_pattern); + + // transports have a particular pattern + if !transport_pattern.is_match(&transport) { + return Err(BridgeLineError::TransportNameInvalid(transport)); + } + + // port can't be 0 + if address.port() == 0 { + return Err(BridgeLineError::AddressPortInvalid); + } + + static BRIDGE_FINGERPRINT_PATTERN: OnceLock = OnceLock::new(); + let bridge_fingerprint_pattern = BRIDGE_FINGERPRINT_PATTERN + .get_or_init(|| Regex::new(r"(?m)^[0-9a-fA-F]{40}$").unwrap()); + + // fingerprint should be a sha1 hash + if !bridge_fingerprint_pattern.is_match(&fingerprint) { + return Err(BridgeLineError::FingerprintInvalid(fingerprint)); + } + + // validate key-values + for (key, value) in &keyvalues { + if key.contains(' ') || key.contains('=') || key.len() == 0 { + return Err(BridgeLineError::KeyValueInvalid(format!("{key}={value}"))); + } + } + + Ok(Self { + transport, + address, + fingerprint, + keyvalues, + }) + } + + /// Get a reference to this `BridgeLine`'s transport field. + pub fn transport(&self) -> &String { + &self.transport + } + + /// Get a reference to this `BridgeLine`'s address field. + pub fn address(&self) -> &SocketAddr { + &self.address + } + + /// Get a reference to this `BridgeLine`'s fingerprint field. + pub fn fingerprint(&self) -> &String { + &self.fingerprint + } + + /// Get a reference to this `BridgeLine`'s key/values field. + pub fn keyvalues(&self) -> &Vec<(String, String)> { + &self.keyvalues + } + + #[cfg(feature = "legacy-tor-provider")] + /// Serialise this `BridgeLine` to the value set via `SETCONF Bridge...` legacy c-tor control-port command. + pub fn as_legacy_tor_setconf_value(&self) -> String { + let transport = &self.transport; + let address = self.address.to_string(); + let fingerprint = self.fingerprint.to_string(); + let keyvalues: Vec = self + .keyvalues + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect(); + let keyvalues = keyvalues.join(" "); + + format!("{transport} {address} {fingerprint} {keyvalues}") + } +} + +impl FromStr for BridgeLine { + type Err = BridgeLineError; + fn from_str(s: &str) -> Result { + let mut tokens = s.split(' '); + // get transport name + let transport = if let Some(transport) = tokens.next() { + transport + } else { + return Err(BridgeLineError::TransportMissing(s.to_string())); + }; + // get bridge address + let address = if let Some(address) = tokens.next() { + if let Ok(address) = SocketAddr::from_str(address) { + address + } else { + return Err(BridgeLineError::AddressParseFailed(address.to_string())); + } + } else { + return Err(BridgeLineError::AddressMissing(s.to_string())); + }; + // get the bridge fingerprint + let fingerprint = if let Some(fingerprint) = tokens.next() { + fingerprint + } else { + return Err(BridgeLineError::FingerprintMissing(s.to_string())); + }; + + // get the bridge options + static BRIDGE_OPTION_PATTERN: OnceLock = OnceLock::new(); + let bridge_option_pattern = BRIDGE_OPTION_PATTERN + .get_or_init(|| Regex::new(r"(?m)^(?[^=]+)=(?.*)$").unwrap()); + + let mut keyvalues: Vec<(String, String)> = Default::default(); + while let Some(keyvalue) = tokens.next() { + if let Some(caps) = bridge_option_pattern.captures(&keyvalue) { + let key = caps + .name("key") + .expect("missing key group") + .as_str() + .to_string(); + let value = caps + .name("value") + .expect("missing value group") + .as_str() + .to_string(); + keyvalues.push((key, value)); + } else { + return Err(BridgeLineError::KeyValueInvalid(keyvalue.to_string())); + } + } + + BridgeLine::new( + transport.to_string(), + address, + fingerprint.to_string(), + keyvalues, + ) + } +} \ No newline at end of file diff --git a/tor-interface/src/legacy_tor_client.rs b/tor-interface/src/legacy_tor_client.rs new file mode 100644 index 000000000..b3e0681e8 --- /dev/null +++ b/tor-interface/src/legacy_tor_client.rs @@ -0,0 +1,804 @@ +// standard +use std::collections::BTreeMap; +use std::convert::From; +use std::default::Default; +use std::net::{IpAddr, SocketAddr, TcpListener}; +use std::option::Option; +use std::path::PathBuf; +use std::str::FromStr; +use std::string::ToString; +use std::sync::{atomic, Arc}; +use std::time::Duration; +use zeroize::ZeroizeOnDrop; +#[cfg(unix)] +use std::os::unix::net::SocketAddr as UnixSocketAddr; + +// extern crates +use socks::{SocketAddrOrUnixSocketAddr, Socks5Stream}; + +// internal crates +use crate::censorship_circumvention::*; +use crate::legacy_tor_control_stream::*; +use crate::legacy_tor_controller::*; +use crate::legacy_tor_process::*; +use crate::legacy_tor_version::*; +use crate::proxy::*; +use crate::tor_crypto::*; +use crate::tor_provider; +use crate::tor_provider::*; + +/// [`LegacyTorClient`]-specific error type +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("failed to create LegacyTorProcess object")] + LegacyTorProcessCreationFailed(#[source] crate::legacy_tor_process::Error), + + #[error("failed to create LegacyControlStream object")] + LegacyControlStreamCreationFailed(#[source] crate::legacy_tor_control_stream::Error), + + #[error("failed to create LegacyTorController object")] + LegacyTorControllerCreationFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to authenticate with the tor process")] + LegacyTorProcessAuthenticationFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to determine the tor process version")] + GetInfoVersionFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("tor process version to old; found {0} but must be at least {1}")] + LegacyTorProcessTooOld(String, String), + + #[error("failed to register for STATUS_CLIENT and HS_DESC events")] + SetEventsFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to delete unused onion service")] + DelOnionFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed waiting for async events: {0}")] + WaitAsyncEventsFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to begin bootstrap")] + SetConfDisableNetwork0Failed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to setconf")] + SetConfFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to add client auth for onion service")] + OnionClientAuthAddFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to remove client auth from onion service")] + OnionClientAuthRemoveFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("failed to get socks listener")] + GetInfoNetListenersSocksFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("no socks listeners available to connect through")] + NoSocksListenersFound(), + + #[error("invalid circuit token")] + CircuitTokenInvalid(), + + #[error("unable to read cookie file: {1:?}")] + CookieReadingFailed(#[source] std::io::Error, PathBuf), + + #[error("unable to connect to socks listener")] + Socks5ConnectionFailed(#[source] std::io::Error), + + #[error("unable to bind TCP listener")] + TcpListenerBindFailed(#[source] std::io::Error), + + #[error("unable to get TCP listener's local address")] + TcpListenerLocalAddrFailed(#[source] std::io::Error), + + #[error("failed to create onion service")] + AddOnionFailed(#[source] crate::legacy_tor_controller::Error), + + #[error("tor not bootstrapped")] + LegacyTorNotBootstrapped(), + + #[error("{0}")] + PluggableTransportConfigDirectoryCreationFailed(#[source] std::io::Error), + + #[error("unable to create pluggable-transport directory because file with same name already exists: {0:?}")] + PluggableTransportDirectoryNameCollision(PathBuf), + + #[error("{0}")] + PluggableTransportSymlinkRemovalFailed(#[source] std::io::Error), + + #[error("{0}")] + PluggableTransportSymlinkCreationFailed(#[source] std::io::Error), + + #[error("pluggable transport binary name not representable as utf8: {0:?}")] + PluggableTransportBinaryNameNotUtf8Representnable(std::ffi::OsString), + + #[error("{0}")] + PluggableTransportConfigError(#[source] crate::censorship_circumvention::PluggableTransportConfigError), + + #[error("pluggable transport multiply defines '{0}' bridge transport type")] + BridgeTransportTypeMultiplyDefined(String), + + #[error("bridge transport '{0}' not supported by pluggable transport configuration")] + BridgeTransportNotSupported(String), + + #[error("not implemented")] + NotImplemented(), +} + +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + +// +// CircuitToken Implementation +// +struct LegacyCircuitToken { + username: String, + password: String, +} + +impl LegacyCircuitToken { + fn new() -> LegacyCircuitToken { + const CIRCUIT_TOKEN_USERNAME_LENGTH: usize = 32usize; + const CIRCUIT_TOKEN_PASSWORD_LENGTH: usize = 32usize; + let username = generate_password(CIRCUIT_TOKEN_USERNAME_LENGTH); + let password = generate_password(CIRCUIT_TOKEN_PASSWORD_LENGTH); + + LegacyCircuitToken { username, password } + } +} + +impl Default for LegacyCircuitToken { + fn default() -> Self { + Self::new() + } +} + +// +// LegacyTorClientConfig +// + +#[derive(Clone, Debug)] +pub enum LegacyTorClientConfig { + BundledTor { + tor_bin_path: PathBuf, + data_directory: PathBuf, + proxy_settings: Option, + allowed_ports: Option>, + pluggable_transports: Option>, + bridge_lines: Option>, + }, + SystemTor { + tor_socks_addr: SocketAddrOrUnixSocketAddr, + tor_control_addr: SocketAddrOrUnixSocketAddr, + tor_control_auth: Option, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, ZeroizeOnDrop)] +pub enum TorAuth { + Password(String), + #[zeroize(skip)] + Cookie(PathBuf), + CookieData([u8; 32]), +} + +impl LegacyTorClientConfig { + fn one(ipc: &str, host: &str, port: &str) -> Option { + #[cfg(unix)] + if let Some(p) = std::env::var_os(ipc) { + return Some(UnixSocketAddr::from_pathname(p).ok()?.into()); + } + match (std::env::var(host), std::env::var(port)) { + (Ok(h), Ok(p)) => Some(SocketAddr::new(IpAddr::from_str(&h).ok()?, u16::from_str(&p).ok()?).into()), + _ => None, + } + } + + /// Consult `$TOR_SOCKS_{IPC_PATH,HOST+PORT}`, `$TOR_CONTROL_{IPC_PATH,HOST+PORT}` and `$TOR_CONTROL_{PASSWD,COOKIE_AUTH_FILE}` + /// + /// `$TOR_SOCKS_IPC_PATH` and `$TOR_CONTROL_IPC_PATH` are ignored if `cfg(not(unix))`, + /// and take precedence if `cfg(unix)`. + /// + /// `$TOR_CONTROL_PASSWD` takes precedence over `$TOR_CONTROL_COOKIE_AUTH_FILE` + pub fn system_from_environment() -> Option { + Some(LegacyTorClientConfig::SystemTor { + tor_socks_addr: Self::one("TOR_SOCKS_IPC_PATH", "TOR_SOCKS_HOST", "TOR_SOCKS_PORT")?, + tor_control_addr: Self::one("TOR_CONTROL_IPC_PATH", "TOR_CONTROL_HOST", "TOR_CONTROL_PORT")?, + tor_control_auth: match (std::env::var("TOR_CONTROL_PASSWD"), std::env::var_os("TOR_CONTROL_COOKIE_AUTH_FILE")) { + (Ok(pass), _) => Some(TorAuth::Password(pass)), + (Err(std::env::VarError::NotUnicode(_)), _) => return None, + (_, Some(cookie)) => Some(TorAuth::Cookie(cookie.into())), + _ => None, + } + }) + } +} + +#[test] +fn system_from_environment() { + fn flatten(conf: Option) -> Option<(SocketAddrOrUnixSocketAddr, SocketAddrOrUnixSocketAddr, Option)> { + conf.and_then(|c| match c { + LegacyTorClientConfig::BundledTor { .. } => None, + LegacyTorClientConfig::SystemTor { tor_socks_addr, tor_control_addr, tor_control_auth } => Some((tor_socks_addr, tor_control_addr, tor_control_auth)) + }) + } + + for var in ["TOR_SOCKS_IPC_PATH", "TOR_SOCKS_HOST", "TOR_SOCKS_PORT", "TOR_CONTROL_IPC_PATH", "TOR_CONTROL_HOST", "TOR_CONTROL_PORT", "TOR_CONTROL_PASSWD", "TOR_CONTROL_COOKIE_AUTH_FILE"] { + unsafe { std::env::remove_var(var) }; + } + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), None); + + std::env::set_var("TOR_SOCKS_HOST", "1.1.1.1"); + std::env::set_var("TOR_SOCKS_PORT", "9050"); + std::env::set_var("TOR_CONTROL_HOST", "2.2.2.2"); + std::env::set_var("TOR_CONTROL_PORT", "9051"); + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + None, + ))); + + unsafe { std::env::set_var("TOR_CONTROL_PASSWD", std::ffi::OsStr::from_encoded_bytes_unchecked(b"\xFF")) }; + std::env::set_var("TOR_CONTROL_COOKIE_AUTH_FILE", "/cookie"); + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), None); + + std::env::set_var("TOR_CONTROL_PASSWD", "pass"); + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Password("pass".to_string())), + ))); + + unsafe { std::env::remove_var("TOR_CONTROL_PASSWD") }; + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Cookie("/cookie".into())), + ))); + + std::env::set_var("TOR_SOCKS_IPC_PATH", "/sock"); + #[cfg(not(unix))] + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + SocketAddr::from(([1, 1, 1, 1], 9050)).into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Cookie("/cookie".into())), + ))); + #[cfg(unix)] + assert_eq!(flatten(LegacyTorClientConfig::system_from_environment()), Some(( + UnixSocketAddr::from_pathname("/sock").unwrap().into(), + SocketAddr::from(([2, 2, 2, 2], 9051)).into(), + Some(TorAuth::Cookie("/cookie".into())), + ))); +} + +// +// LegacyTorClient +// + +/// A `LegacyTorClient` implements the [`TorProvider`] trait using a legacy c-tor daemon backend. +/// +/// The tor process can either be launched and owned by `LegacyTorClient`, or it can use an already running tor-daemon. When using an already runnng tor-daemon, the [`TorProvider::bootstrap()`] automatically succeeds, presuming the connected tor-daemon has successfully bootstrapped. +/// +/// The minimum supported c-tor is version 0.4.6.1. +pub struct LegacyTorClient { + daemon: Option, + version: LegacyTorVersion, + controller: LegacyTorController, + bootstrapped: bool, + socks_listener: Option, + // list of open onion services and their is_active flag + onion_services: Vec<(V3OnionServiceId, Arc)>, + // our list of circuit tokens for the tor daemon + circuit_token_counter: usize, + circuit_tokens: BTreeMap, +} + +impl LegacyTorClient { + /// Construct a new `LegacyTorClient` from a [`LegacyTorClientConfig`]. + pub fn new(mut config: LegacyTorClientConfig) -> Result { + let (daemon, mut controller, mut auth, socks_listener) = match &mut config { + LegacyTorClientConfig::BundledTor { + tor_bin_path, + data_directory, + .. + } => { + // launch tor + let daemon = + LegacyTorProcess::new(tor_bin_path.as_path(), data_directory.as_path()) + .map_err(Error::LegacyTorProcessCreationFailed)?; + // open a control stream + let control_stream = + LegacyControlStream::new(daemon.get_control_addr(), Duration::from_millis(16)) + .map_err(Error::LegacyControlStreamCreationFailed)?; + + // create a controler + let controller = LegacyTorController::new(control_stream) + .map_err(Error::LegacyTorControllerCreationFailed)?; + + let password = daemon.get_password().to_string(); + (Some(daemon), controller, Some(TorAuth::Password(password)), None) + } + LegacyTorClientConfig::SystemTor { + tor_socks_addr, + tor_control_addr, + tor_control_auth, + } => { + // open a control stream + let control_stream = LegacyControlStream::new(tor_control_addr, Duration::from_millis(16)) + .map_err(Error::LegacyControlStreamCreationFailed)?; + + // create a controler + let controller = LegacyTorController::new(control_stream) + .map_err(Error::LegacyTorControllerCreationFailed)?; + + ( + None, + controller, + tor_control_auth.take(), + Some(tor_socks_addr.clone().into()), + ) + } + }; + + // authenticate + match auth.as_mut() { + None => controller.authenticate_auto(), + Some(TorAuth::Password(pass)) => controller.authenticate(&pass), + Some(TorAuth::Cookie(file)) => controller.authenticate_cookie(crate::legacy_tor_controller::read_cookie(&file).map_err(|e| Error::CookieReadingFailed(e, std::mem::take(file)))?), + Some(TorAuth::CookieData(cookie)) => controller.authenticate_cookie(*cookie), + }.map_err(Error::LegacyTorProcessAuthenticationFailed)?; + + // min required version for v3 client auth (see control-spec.txt) + let min_required_version = LegacyTorVersion { + major: 0u32, + minor: 4u32, + micro: 6u32, + patch_level: 1u32, + status_tag: None, + }; + + // verify version is recent enough + let version = controller + .getinfo_version() + .map_err(Error::GetInfoVersionFailed)?; + + if version < min_required_version { + return Err(Error::LegacyTorProcessTooOld( + version.to_string(), + min_required_version.to_string(), + )); + } + + // configure tor client + if let LegacyTorClientConfig::BundledTor { + data_directory, + proxy_settings, + allowed_ports, + pluggable_transports, + bridge_lines, + .. + } = config + { + // configure proxy + match proxy_settings { + Some(ProxyConfig::Socks4(Socks4ProxyConfig { address })) => { + controller + .setconf(&[("Socks4Proxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; + } + Some(ProxyConfig::Socks5(Socks5ProxyConfig { + address, + username, + password, + })) => { + controller + .setconf(&[("Socks5Proxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; + let username = username.unwrap_or("".to_string()); + if !username.is_empty() { + controller + .setconf(&[("Socks5ProxyUsername", username.to_string())]) + .map_err(Error::SetConfFailed)?; + } + let password = password.unwrap_or("".to_string()); + if !password.is_empty() { + controller + .setconf(&[("Socks5ProxyPassword", password.to_string())]) + .map_err(Error::SetConfFailed)?; + } + } + Some(ProxyConfig::Https(HttpsProxyConfig { + address, + username, + password, + })) => { + controller + .setconf(&[("HTTPSProxy", address.to_string())]) + .map_err(Error::SetConfFailed)?; + let username = username.unwrap_or("".to_string()); + let password = password.unwrap_or("".to_string()); + if !username.is_empty() || !password.is_empty() { + let authenticator = format!("{}:{}", username, password); + controller + .setconf(&[("HTTPSProxyAuthenticator", authenticator)]) + .map_err(Error::SetConfFailed)?; + } + } + None => (), + } + // configure firewall + if let Some(allowed_ports) = allowed_ports { + let allowed_addresses: Vec = allowed_ports + .iter() + .map(|port| format!("*{{}}:{port}")) + .collect(); + let allowed_addresses = allowed_addresses.join(", "); + controller + .setconf(&[("ReachableAddresses", allowed_addresses)]) + .map_err(Error::SetConfFailed)?; + } + // configure pluggable transports + let mut supported_transports: std::collections::BTreeSet = Default::default(); + if let Some(pluggable_transports) = pluggable_transports { + // Legacy tor daemon cannot be configured to use pluggable-transports which + // exist in paths containing spaces. To work around this, we create a known, safe + // path in the tor daemon's working directory, and soft-link the provided + // binary path to this safe location. Finally, we configure tor to use the soft-linked + // binary in the ClientTransportPlugin setconf call. + + // create pluggable-transport directory + let mut pt_directory = data_directory.clone(); + pt_directory.push("pluggable-transports"); + if !std::path::Path::exists(&pt_directory) { + // path does not exist so create it + std::fs::create_dir(&pt_directory) + .map_err(Error::PluggableTransportConfigDirectoryCreationFailed)?; + } else if !std::path::Path::is_dir(&pt_directory) { + // path exists but it is not a directory + return Err(Error::PluggableTransportDirectoryNameCollision( + pt_directory, + )); + } + + // symlink all our pts and configure tor + let mut conf: Vec<(&str, String)> = Default::default(); + for pt_settings in &pluggable_transports { + // symlink absolute path of pt binary to pt_directory in tor's working + // directory + let path_to_binary = pt_settings.path_to_binary(); + let binary_name = path_to_binary + .file_name() + .expect("file_name should be absolute path"); + let mut pt_symlink = pt_directory.clone(); + pt_symlink.push(binary_name); + let binary_name = if let Some(binary_name) = binary_name.to_str() { + binary_name + } else { + return Err(Error::PluggableTransportBinaryNameNotUtf8Representnable( + binary_name.to_os_string(), + )); + }; + + // remove any file that may exist with the same name + if std::path::Path::exists(&pt_symlink) { + std::fs::remove_file(&pt_symlink) + .map_err(Error::PluggableTransportSymlinkRemovalFailed)?; + } + + // create new symlink + #[cfg(windows)] + std::os::windows::fs::symlink_file(path_to_binary, &pt_symlink) + .map_err(Error::PluggableTransportSymlinkCreationFailed)?; + #[cfg(unix)] + std::os::unix::fs::symlink(path_to_binary, &pt_symlink) + .map_err(Error::PluggableTransportSymlinkCreationFailed)?; + + // verify a bridge-type support has not been defined for multiple pluggable-transports + for transport in pt_settings.transports() { + if supported_transports.contains(transport) { + return Err(Error::BridgeTransportTypeMultiplyDefined( + transport.to_string(), + )); + } + supported_transports.insert(transport.to_string()); + } + + // finally construct our setconf value + let transports = pt_settings.transports().join(","); + use std::path::MAIN_SEPARATOR; + let path_to_binary = + format!("pluggable-transports{MAIN_SEPARATOR}{binary_name}"); + let options = pt_settings.options().join(" "); + + let value = format!("{transports} exec {path_to_binary} {options}"); + conf.push(("ClientTransportPlugin", value)); + } + controller + .setconf(conf.as_slice()) + .map_err(Error::SetConfFailed)?; + } + // configure bridge lines + if let Some(bridge_lines) = bridge_lines { + let mut conf: Vec<(&str, String)> = Default::default(); + for bridge_line in &bridge_lines { + if !supported_transports.contains(bridge_line.transport()) { + return Err(Error::BridgeTransportNotSupported( + bridge_line.transport().to_string(), + )); + } + let value = bridge_line.as_legacy_tor_setconf_value(); + conf.push(("Bridge", value)); + } + conf.push(("UseBridges", "1".to_string())); + controller + .setconf(conf.as_slice()) + .map_err(Error::SetConfFailed)?; + } + } + + // register for STATUS_CLIENT async events + controller + .setevents(&["STATUS_CLIENT", "HS_DESC"]) + .map_err(Error::SetEventsFailed)?; + + Ok(LegacyTorClient { + daemon, + version, + controller, + bootstrapped: false, + socks_listener, + onion_services: Default::default(), + circuit_token_counter: 0usize, + circuit_tokens: Default::default(), + }) + } + + /// Get the version of the connected c-tor daemon. + pub fn version(&mut self) -> LegacyTorVersion { + self.version.clone() + } +} + +impl TorProvider for LegacyTorClient { + type Stream = TcpOrUnixOnionStream; + type Listener = TcpOnionListener; + + fn update(&mut self) -> Result, tor_provider::Error> { + let mut i = 0; + while i < self.onion_services.len() { + // remove onion services with no active listeners + if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) { + let entry = self.onion_services.swap_remove(i); + let service_id = entry.0; + + self.controller + .del_onion(&service_id) + .map_err(Error::DelOnionFailed)?; + } else { + i += 1; + } + } + + let mut events: Vec = Default::default(); + for async_event in self + .controller + .wait_async_events() + .map_err(Error::WaitAsyncEventsFailed)? + { + match async_event { + AsyncEvent::StatusClient { + severity, + action, + arguments, + } => { + if severity == "NOTICE" && action == "BOOTSTRAP" { + let mut progress: u32 = 0; + let mut tag: String = Default::default(); + let mut summary: String = Default::default(); + for (key, val) in arguments { + match key.as_str() { + "PROGRESS" => progress = val.parse().unwrap_or(0u32), + "TAG" => tag = val, + "SUMMARY" => summary = val, + _ => {} // ignore unexpected arguments + } + } + events.push(TorEvent::BootstrapStatus { + progress, + tag, + summary, + }); + if progress == 100u32 { + events.push(TorEvent::BootstrapComplete); + self.bootstrapped = true; + } + } + } + AsyncEvent::HsDesc { action, hs_address } => { + if action == "UPLOADED" { + events.push(TorEvent::OnionServicePublished { + service_id: hs_address, + }); + } + } + AsyncEvent::Unknown { lines } => { + println!("Received Unknown Event:"); + for line in lines.iter() { + println!(" {}", line); + } + } + } + } + + if let Some(daemon) = &mut self.daemon { + // bundled tor gives us log-lines + for log_line in daemon.wait_log_lines().iter_mut() { + events.push(TorEvent::LogReceived { + line: std::mem::take(log_line), + }); + } + } else if !self.bootstrapped { + // system tor needs to send a bootstrap complete event *once* + events.push(TorEvent::BootstrapComplete); + self.bootstrapped = true; + } + + Ok(events) + } + + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { + if !self.bootstrapped { + self.controller + .setconf(&[("DisableNetwork", "0".to_string())]) + .map_err(Error::SetConfDisableNetwork0Failed)?; + } + Ok(()) + } + + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), tor_provider::Error> { + Ok(self + .controller + .onion_client_auth_add(service_id, client_auth, None, &Default::default()) + .map_err(Error::OnionClientAuthAddFailed)?) + } + + fn remove_client_auth( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { + Ok(self + .controller + .onion_client_auth_remove(service_id) + .map_err(Error::OnionClientAuthRemoveFailed)?) + } + + // connect to an onion service and returns OnionStream + fn connect( + &mut self, + target: TargetAddr, + circuit: Option, + ) -> Result { + if !self.bootstrapped { + return Err(Error::LegacyTorNotBootstrapped().into()); + } + + if self.socks_listener.is_none() { + let mut listeners = self + .controller + .getinfo_net_listeners_socks() + .map_err(Error::GetInfoNetListenersSocksFailed)?; + if listeners.is_empty() { + return Err(Error::NoSocksListenersFound())?; + } + self.socks_listener = Some(listeners.swap_remove(0).into()); + } + + let socks_listener = match self.socks_listener.as_ref() { + Some(socks_listener) => socks_listener, + None => unreachable!(), + }; + + // our target + let socks_target = match target.clone() { + TargetAddr::Socket(socket_addr) => socks::TargetAddr::Ip(socket_addr), + TargetAddr::Domain(domain_addr) => { + socks::TargetAddr::Domain(domain_addr.domain().to_string(), domain_addr.port()) + } + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { + service_id, + virt_port, + })) => socks::TargetAddr::Domain(format!("{}.onion", service_id), virt_port), + }; + + // readwrite stream + let stream = match &circuit { + None => Socks5Stream::connect_either(socks_listener, socks_target), + Some(circuit) => { + if let Some(circuit) = self.circuit_tokens.get(circuit) { + Socks5Stream::connect_either_with_password( + socks_listener, + socks_target, + &circuit.username, + &circuit.password, + ) + } else { + return Err(Error::CircuitTokenInvalid())?; + } + } + } + .map_err(Error::Socks5ConnectionFailed)?; + + Ok(TcpOrUnixOnionStream { + stream: stream.into_inner(), + local_addr: None, + peer_addr: Some(target), + }) + } + + // stand up an onion service and return an OnionListener + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, + ) -> Result { + if !self.bootstrapped { + return Err(Error::LegacyTorNotBootstrapped().into()); + } + + // try to bind to a local address, let OS pick our port + let listener = TcpListener::bind(bind_addr.unwrap_or(([127, 0, 0, 1], 0u16).into())).map_err(Error::TcpListenerBindFailed)?; + let bind_addr = listener + .local_addr() + .map_err(Error::TcpListenerLocalAddrFailed)?; + + let flags = AddOnionFlags { + discard_pk: true, + v3_auth: authorized_clients.is_some(), + ..Default::default() + }; + + // start onion service + let (_, service_id) = self + .controller + .add_onion( + Some(private_key), + &flags, + None, + virt_port, + Some(bind_addr), + authorized_clients, + ) + .map_err(Error::AddOnionFailed)?; + + let onion_addr = OnionAddr::V3(OnionAddrV3::new( + V3OnionServiceId::from_private_key(private_key), + virt_port, + )); + + let is_active = Arc::new(atomic::AtomicBool::new(true)); + self.onion_services + .push((service_id, Arc::clone(&is_active))); + + Ok(TcpOnionListener(TcpOnionListenerBase(listener, onion_addr), is_active)) + } + + fn generate_token(&mut self) -> CircuitToken { + let new_token = self.circuit_token_counter; + self.circuit_token_counter += 1; + self.circuit_tokens + .insert(new_token, LegacyCircuitToken::new()); + new_token + } + + fn release_token(&mut self, circuit_token: CircuitToken) { + self.circuit_tokens.remove(&circuit_token); + } +} diff --git a/tor-interface/src/legacy_tor_control_stream.rs b/tor-interface/src/legacy_tor_control_stream.rs new file mode 100644 index 000000000..d7b4610a1 --- /dev/null +++ b/tor-interface/src/legacy_tor_control_stream.rs @@ -0,0 +1,277 @@ +// standard +use std::collections::VecDeque; +use std::default::Default; +use std::io::{ErrorKind, Read, Write, IoSlice}; +use std::option::Option; +use std::string::ToString; +use std::time::Duration; + +// extern crates +use regex::Regex; +use socks::{SocketAddrOrUnixSocketAddr, TcpOrUnixStream}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("control stream read timeout must not be zero")] + ReadTimeoutZero(), + + #[error("could not connect to control port")] + CreationFailed(#[source] std::io::Error), + + #[error("configure control port socket failed")] + ConfigurationFailed(#[source] std::io::Error), + + #[error("control port parsing regex creation failed")] + ParsingRegexCreationFailed(#[source] regex::Error), + + #[error("control port stream read failure")] + ReadFailed(#[source] std::io::Error), + + #[error("control port stream closed by remote")] + ClosedByRemote(), + + #[error("received control port response invalid utf8")] + InvalidResponse(#[source] std::str::Utf8Error), + + #[error("failed to parse control port reply: {0}")] + ReplyParseFailed(String), + + #[error("control port stream write failure")] + WriteFailed(#[source] std::io::Error), +} + +pub(crate) struct LegacyControlStream { + stream: TcpOrUnixStream, + closed_by_remote: bool, + pending_data: Vec, + pending_lines: VecDeque, + pending_reply: Vec, + reading_multiline_value: bool, + // regexes used to parse control port responses + single_line_data: Regex, + multi_line_data: Regex, + end_reply_line: Regex, +} + +type StatusCode = u32; +pub(crate) struct Reply { + pub status_code: StatusCode, + pub reply_lines: Vec, +} + +impl LegacyControlStream { + pub fn new>(addr: T, read_timeout: Duration) -> Result { + if read_timeout.is_zero() { + return Err(Error::ReadTimeoutZero()); + } + + let stream = TcpOrUnixStream::connect(addr).map_err(Error::CreationFailed)?; + stream + .set_read_timeout(Some(read_timeout)) + .map_err(Error::ConfigurationFailed)?; + + // pre-allocate a kilobyte for the read buffer + const READ_BUFFER_SIZE: usize = 1024; + let pending_data = Vec::with_capacity(READ_BUFFER_SIZE); + + let single_line_data = + Regex::new(r"^\d\d\d-.*").map_err(Error::ParsingRegexCreationFailed)?; + let multi_line_data = + Regex::new(r"^\d\d\d+.*").map_err(Error::ParsingRegexCreationFailed)?; + let end_reply_line = + Regex::new(r"^\d\d\d .*").map_err(Error::ParsingRegexCreationFailed)?; + + Ok(LegacyControlStream { + stream, + closed_by_remote: false, + pending_data, + pending_lines: Default::default(), + pending_reply: Default::default(), + reading_multiline_value: false, + // regex + single_line_data, + multi_line_data, + end_reply_line, + }) + } + + #[cfg(test)] + pub(crate) fn closed_by_remote(&mut self) -> bool { + self.closed_by_remote + } + + fn read_line(&mut self) -> Result, Error> { + // read pending bytes from stream until we have a line to return + while self.pending_lines.is_empty() { + let byte_count = self.pending_data.len(); + match self.stream.read_to_end(&mut self.pending_data) { + Err(err) => { + if err.kind() == ErrorKind::WouldBlock || err.kind() == ErrorKind::TimedOut { + if byte_count == self.pending_data.len() { + return Ok(None); + } + } else { + return Err(Error::ReadFailed(err)); + } + } + Ok(0usize) => { + self.closed_by_remote = true; + return Err(Error::ClosedByRemote()); + } + Ok(_count) => (), + } + + // split our read buffer into individual lines + let mut begin = 0; + for index in 1..self.pending_data.len() { + if self.pending_data[index - 1] == b'\r' && self.pending_data[index] == b'\n' { + let end = index - 1; + // view into byte vec of just the found line + let line_view: &[u8] = &self.pending_data[begin..end]; + // convert to string + let line_string = + std::str::from_utf8(line_view).map_err(Error::InvalidResponse)?; + + // save in pending list + self.pending_lines.push_back(line_string.to_string()); + // update begin (and skip over \r\n) + begin = end + 2; + } + } + // leave any leftover bytes in the buffer for the next call + self.pending_data.drain(0..begin); + } + + Ok(self.pending_lines.pop_front()) + } + + pub fn read_reply(&mut self) -> Result, Error> { + loop { + let current_line = match self.read_line()? { + Some(line) => line, + None => return Ok(None), + }; + + // make sure the status code matches (if we are not in the + // middle of a multi-line read + if let Some(first_line) = self.pending_reply.first() { + if !self.reading_multiline_value { + let first_status_code = &first_line[0..3]; + let current_status_code = ¤t_line[0..3]; + if first_status_code != current_status_code { + return Err(Error::ReplyParseFailed(format!( + "mismatched status codes, {} != {}", + first_status_code, current_status_code + ))); + } + } + } + + // end of a response + if self.end_reply_line.is_match(¤t_line) { + if self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found multi-line end reply but not reading a multi-line reply".to_string(), + )); + } + self.pending_reply.push(current_line); + break; + // single line data from getinfo and friends + } else if self.single_line_data.is_match(¤t_line) { + if self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found single-line reply but still reading a multi-line reply".to_string(), + )); + } + self.pending_reply.push(current_line); + // begin of multiline data from getinfo and friends + } else if self.multi_line_data.is_match(¤t_line) { + if self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found multi-line start reply but still reading a multi-line reply" + .to_string(), + )); + } + self.pending_reply.push(current_line); + self.reading_multiline_value = true; + // multiline data to be squashed to a single entry + } else { + if !self.reading_multiline_value { + return Err(Error::ReplyParseFailed( + "found a multi-line intermediate reply but not reading a multi-line reply" + .to_string(), + )); + } + // don't bother writing the end of multiline token + if current_line == "." { + self.reading_multiline_value = false; + } else { + let multiline = match self.pending_reply.last_mut() { + Some(multiline) => multiline, + // if our logic here is right, then + // self.reading_multiline_value == !self.pending_reply.is_empty() + // should always be true regardless of the data received + // from the control port + None => unreachable!(), + }; + multiline.push('\n'); + multiline.push_str(¤t_line); + } + } + } + + // take ownership of the reply lines + let mut reply_lines: Vec = Default::default(); + std::mem::swap(&mut self.pending_reply, &mut reply_lines); + + // parse out the response code for easier matching + let status_code_string = match reply_lines.first() { + Some(line) => line[0..3].to_string(), + // the lines have already been parsed+validated in the above loop + None => unreachable!(), + }; + let status_code: u32 = match status_code_string.parse() { + Ok(status_code) => status_code, + Err(_) => { + return Err(Error::ReplyParseFailed(format!( + "unable to parse '{}' as status code", + status_code_string + ))) + } + }; + + // strip the redundant status code from start of lines + for line in reply_lines.iter_mut() { + if line.starts_with(&status_code_string) { + *line = line[4..].to_string(); + } + } + + Ok(Some(Reply { + status_code, + reply_lines, + })) + } + + pub fn write(&mut self, cmd: &str) -> Result<(), Error> { + if let Err(err) = write_all_vectored(&mut self.stream, &mut [IoSlice::new(cmd.as_bytes()), IoSlice::new(b"\r\n")]) { + self.closed_by_remote = true; + return Err(Error::WriteFailed(err)); + } + Ok(()) + } +} + +// Implementation taken from std::io::Read::write_all_vectored() +// TODO: remove once stabilised +fn write_all_vectored(write: &mut W, mut bufs: &mut [IoSlice<'_>]) -> std::io::Result<()> { + while !bufs.is_empty() { + match write.write_vectored(bufs) { + Ok(0) => return Err(std::io::Error::new(std::io::ErrorKind::WriteZero, "failed to write whole buffer")), + Ok(n) => IoSlice::advance_slices(&mut bufs, n), + Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } + Ok(()) +} diff --git a/tor-interface/src/legacy_tor_controller.rs b/tor-interface/src/legacy_tor_controller.rs new file mode 100644 index 000000000..eaf65ecd3 --- /dev/null +++ b/tor-interface/src/legacy_tor_controller.rs @@ -0,0 +1,1095 @@ +// standard +use std::default::Default; +use std::net::SocketAddr; +use std::option::Option; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::string::ToString; +#[cfg(test)] +use std::time::{Duration, Instant}; + +// extern crates +use hmac::Mac; +use rand::rngs::OsRng; +use rand::TryRngCore; +use regex::Regex; +#[cfg(test)] +use serial_test::serial; + +// internal crates +use crate::legacy_tor_control_stream::*; +#[cfg(test)] +use crate::legacy_tor_process::*; +use crate::legacy_tor_version::*; +use crate::tor_crypto::*; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("response regex creation failed")] + ParsingRegexCreationFailed(#[source] regex::Error), + + #[error("control stream read reply failed")] + ReadReplyFailed(#[source] crate::legacy_tor_control_stream::Error), + + #[error("unexpected synchronous reply recieved")] + UnexpectedSynchonousReplyReceived(), + + #[error("control stream write command failed")] + WriteCommandFailed(#[source] crate::legacy_tor_control_stream::Error), + + #[error("invalid command arguments: {0}")] + InvalidCommandArguments(String), + + #[error("command failed: {0} {}", .1.join("\n"))] + CommandFailed(u32, Vec), + + #[error("failed to parse command reply: {0}")] + CommandReplyParseFailed(String), + + #[error("failed to parse received tor version")] + TorVersionParseFailed(#[source] crate::legacy_tor_version::Error), + + #[error("unable to read cookie file: {1:?}")] + CookieReadingFailed(#[source] std::io::Error, PathBuf), + + #[error("[SAFE]COOKIE authentication not supported")] + CookiesNotSupported(), + + #[error("impostor sent invalid SAFECOOKIE HMAC")] + BadCookieHash(), + + #[error("failed to generate random data")] + RngError(#[source] ::Error), +} + +// Per-command data +#[derive(Default)] +pub(crate) struct AddOnionFlags { + pub discard_pk: bool, + pub detach: bool, + pub v3_auth: bool, + pub non_anonymous: bool, + pub max_streams_close_circuit: bool, +} + +#[derive(Default)] +pub(crate) struct OnionClientAuthAddFlags { + pub permanent: bool, +} + +pub(crate) enum AsyncEvent { + Unknown { + lines: Vec, + }, + StatusClient { + severity: String, + action: String, + arguments: Vec<(String, String)>, + }, + HsDesc { + action: String, + hs_address: V3OnionServiceId, + }, +} + +#[derive(Default, Debug, PartialEq, Eq)] +struct ProtocolInfo { + auth_cookie: bool, + auth_safecookie: bool, + auth_null: bool, + cookiefile: PathBuf, +} + +pub(crate) struct LegacyTorController { + // underlying control stream + control_stream: LegacyControlStream, + // list of async replies to be handled + async_replies: Vec, + // regex for parsing events + status_event_pattern: Regex, + status_event_argument_pattern: Regex, + hs_desc_pattern: Regex, + protocolinfo_data: Option, + version: Option, +} + +fn quoted_string(string: &str) -> String { + // replace \ with \\ and " with \" + // see: https://spec.torproject.org/control-spec/message-format.html?highlight=QuotedString#description-format + string.replace("\\", "\\\\").replace("\"", "\\\"") +} + + +// All authentication cookies are 32 bytes long. Controllers MUST NOT +// use the contents of a non-32-byte-long file as an authentication +// cookie. +pub(crate) fn read_cookie(from: &Path) -> std::io::Result<[u8; 32]> { + let mut f = std::fs::File::open(from)?; + let mut ret = [0u8; 32]; + f.read_exact(&mut ret[..])?; + let mut nonce = [0u8; 1]; + if f.read_exact(&mut nonce[..]).is_ok() { + Err(std::io::Error::new(std::io::ErrorKind::FileTooLarge, "cookies are 32 bytes")) + } else { + Ok(ret) + } +} + +fn tonibble(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => 0xA + (c - b'a'), + b'A'..=b'F' => 0xA + (c - b'A'), + _ => unreachable!(), + } +} + +fn hmac_sha256(key: &str, blob1: &[u8], blob2: &[u8], blob3: &[u8]) -> hmac::Hmac { + let mut hmac = hmac::Hmac::new_from_slice(key.as_bytes()).unwrap(); + hmac.update(blob1); + hmac.update(blob2); + hmac.update(blob3); + hmac +} + +fn reply_ok(reply: Reply) -> Result { + match reply.status_code { + 250u32 => Ok(reply), + code => Err(Error::CommandFailed(code, reply.reply_lines)), + } +} + + +// https://raw.githubusercontent.com/torproject/torspec/refs/heads/main/control-spec.txt +// 250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/home/nabijaczleweli/.tor/control_auth_cookie" +// 250-AUTH METHODS=HASHEDPASSWORD +// 250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/coo kie \\\" \320\266 \n 2" +// 250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/C/\001\002\003\004\005\006\007\010\t\n\013\014\r\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377" +// 250-AUTH METHODS=NULL +fn parse_auth_methods(auth: &str) -> ProtocolInfo { + let mut ret = ProtocolInfo::default(); + let mut two = auth["AUTH METHODS=".len()..].splitn(2, ' '); + if let Some(methods) = two.next() { + for m in methods.split(',') { + match m { + "COOKIE" => ret.auth_cookie = true, + "SAFECOOKIE" => ret.auth_safecookie = true, + "NULL" => ret.auth_null = true, + _ => {} + } + } + } + let remainder = two.next(); + if (ret.auth_cookie || ret.auth_safecookie) && remainder.map(|r| r.starts_with("COOKIEFILE=\"")).unwrap_or(false) { + let mut remainder = remainder.unwrap()["COOKIEFILE=\"".len()..].as_bytes(); + + let mut path = vec![]; + // https://datatracker.ietf.org/doc/html/rfc2822 qcontent + while let Some(mut byte) = remainder.get(0).copied() { + if byte == b'"' { + break; + } + remainder = &remainder[1..]; + if byte == b'\\' { + let mut consume = 1; + match (remainder.get(0), remainder.get(1), remainder.get(2)) { + (Some(b't'), ..) => byte = b'\t', + (Some(b'n'), ..) => byte = b'\n', + (Some(b'r'), ..) => byte = b'\r', + (Some(b'\"'), ..) => byte = b'\"', + (Some(b'\''), ..) => byte = b'\'', + (Some(b'\\'), ..) => byte = b'\\', + (Some(h @ b'0'..=b'3'), Some(t @ b'0'..=b'7'), Some(u @ b'0'..=b'7')) => { + byte = ((h - b'0') << 6) | ((t - b'0') << 3) | (u - b'0'); + consume = 3; + } + _ => { + path.clear(); + break; + } + } + remainder = &remainder[consume..]; + } + path.push(byte); + } + // On UNIX, paths are sequences of non-0 bytes. We know this. + // On tor/Win32, paths are sequences of ASCII bytes(?): https://101010.pl/@nabijaczleweli/114655491521731646 + #[cfg(unix)] + { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + ret.cookiefile = OsString::from_vec(path.into()).into(); + } + #[cfg(not(unix))] + { + // TODO: string_from_utf8_lossy_owned + ret.cookiefile = String::from_utf8_lossy(&path).into(); + } + } + ret +} + +#[cfg(test)] +#[test] +fn parse_auth_methods_test() { + assert_eq!(parse_auth_methods(r####"AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/home/nabijaczleweli/.tor/control_auth_cookie""####), ProtocolInfo { + auth_cookie: true, + auth_safecookie: true, + auth_null: false, + cookiefile: Path::new("/home/nabijaczleweli/.tor/control_auth_cookie").to_owned(), + }); + assert_eq!(parse_auth_methods(r####"AUTH METHODS=HASHEDPASSWORD"####), ProtocolInfo::default()); + assert_eq!(parse_auth_methods(r####"AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/coo kie \\\" \320\266 \n 2""####), ProtocolInfo { + auth_cookie: true, + auth_safecookie: true, + auth_null: false, + cookiefile: Path::new("/home/nabijaczleweli/.tor/coo kie \\\" ж \n 2").to_owned(), + }); + #[cfg(unix)] + { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + let mut buf = b"/home/nabijaczleweli/.tor/C/"[..].to_owned(); + for b in 1..=0xFF { + buf.push(b); + } + assert_eq!(parse_auth_methods(r####"AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="/home/nabijaczleweli/.tor/C/\001\002\003\004\005\006\007\010\t\n\013\014\r\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377""####), ProtocolInfo { + auth_cookie: true, + auth_safecookie: true, + auth_null: false, + cookiefile: OsString::from_vec(buf).into(), + }); + } + assert_eq!(parse_auth_methods(r####"AUTH METHODS=NULL"####), ProtocolInfo { + auth_null: true, + ..ProtocolInfo::default() + }); +} + +impl LegacyTorController { + pub fn new(control_stream: LegacyControlStream) -> Result { + let status_event_pattern = + Regex::new(r#"^STATUS_CLIENT (?PNOTICE|WARN|ERR) (?P[A-Za-z]+)"#) + .map_err(Error::ParsingRegexCreationFailed)?; + let status_event_argument_pattern = + Regex::new(r#"(?P[A-Z]+)=(?P[A-Za-z0-9_]+|"[^"]+")"#) + .map_err(Error::ParsingRegexCreationFailed)?; + let hs_desc_pattern = Regex::new( + r#"HS_DESC (?PREQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P[a-z2-7]{56})"# + ).map_err(Error::ParsingRegexCreationFailed)?; + + Ok(LegacyTorController { + control_stream, + async_replies: Default::default(), + // regex + status_event_pattern, + status_event_argument_pattern, + hs_desc_pattern, + protocolinfo_data: None, + version: None, + }) + } + + // return curently available events, does not block waiting + // for an event + fn wait_async_replies(&mut self) -> Result, Error> { + let mut replies: Vec = Default::default(); + // take any previously received async replies + std::mem::swap(&mut self.async_replies, &mut replies); + + // and keep consuming until none are available + loop { + if let Some(reply) = self + .control_stream + .read_reply() + .map_err(Error::ReadReplyFailed)? + { + replies.push(reply); + } else { + // no more replies immediately available so return + return Ok(replies); + } + } + } + + fn reply_to_event(&self, reply: &mut Reply) -> Result { + if reply.status_code != 650u32 { + return Err(Error::UnexpectedSynchonousReplyReceived()); + } + + // not sure this is what we want but yolo + let reply_text = reply.reply_lines.join(" "); + if let Some(caps) = self.status_event_pattern.captures(&reply_text) { + let severity = match caps.name("severity") { + Some(severity) => severity.as_str(), + None => unreachable!(), + }; + let action = match caps.name("action") { + Some(action) => action.as_str(), + None => unreachable!(), + }; + + let mut arguments: Vec<(String, String)> = Default::default(); + for caps in self + .status_event_argument_pattern + .captures_iter(&reply_text) + { + let key = match caps.name("key") { + Some(key) => key.as_str(), + None => unreachable!(), + }; + let value = { + let value = match caps.name("value") { + Some(value) => value.as_str(), + None => unreachable!(), + }; + if value.starts_with('\"') && value.ends_with('\"') { + &value[1..value.len() - 1] + } else { + value + } + }; + arguments.push((key.to_string(), value.to_string())); + } + + return Ok(AsyncEvent::StatusClient { + severity: severity.to_string(), + action: action.to_string(), + arguments, + }); + } + + if let Some(caps) = self.hs_desc_pattern.captures(&reply_text) { + let action = match caps.name("action") { + Some(action) => action.as_str(), + None => unreachable!(), + }; + let hs_address = match caps.name("hsaddress") { + Some(hs_address) => hs_address.as_str(), + None => unreachable!(), + }; + + if let Ok(hs_address) = V3OnionServiceId::from_string(hs_address) { + return Ok(AsyncEvent::HsDesc { + action: action.to_string(), + hs_address, + }); + } + } + + // no luck parsing reply, just return full text + let mut reply_lines: Vec = Default::default(); + std::mem::swap(&mut reply_lines, &mut reply.reply_lines); + + Ok(AsyncEvent::Unknown { lines: reply_lines }) + } + + pub fn wait_async_events(&mut self) -> Result, Error> { + let mut async_replies = self.wait_async_replies()?; + let mut async_events: Vec = Default::default(); + + for reply in async_replies.iter_mut() { + async_events.push(self.reply_to_event(reply)?); + } + + Ok(async_events) + } + + // wait for a sync reply, save off async replies for later + fn wait_sync_reply(&mut self) -> Result { + loop { + if let Some(reply) = self + .control_stream + .read_reply() + .map_err(Error::ReadReplyFailed)? + { + match reply.status_code { + 650u32 => self.async_replies.push(reply), + _ => return Ok(reply), + } + } + } + } + + fn write_command(&mut self, text: &str) -> Result { + self.control_stream + .write(text) + .map_err(Error::WriteCommandFailed)?; + self.wait_sync_reply() + } + + // + // Tor Commands + // + // The section where we can find the specification in control-spec.txt + // for the underlying command is listed in parentheses + // + // Each of these command wrapper methods block until completion + // + + // SETCONF (3.1) + fn setconf_cmd(&mut self, key_values: &[(&str, String)]) -> Result { + if key_values.is_empty() { + return Err(Error::InvalidCommandArguments( + "SETCONF key-value pairs list must not be empty".to_string(), + )); + } + let mut command_buffer = vec!["SETCONF".to_string()]; + + for (key, value) in key_values.iter() { + command_buffer.push(format!("{}=\"{}\"", key, quoted_string(value.trim()))); + } + let command = command_buffer.join(" "); + + self.write_command(&command) + } + + // GETCONF (3.3) + #[cfg(test)] + fn getconf_cmd(&mut self, keywords: &[&str]) -> Result { + if keywords.is_empty() { + return Err(Error::InvalidCommandArguments( + "GETCONF keywords list must not be empty".to_string(), + )); + } + let command = format!("GETCONF {}", keywords.join(" ")); + + self.write_command(&command) + } + + // SETEVENTS (3.4) + fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result { + if event_codes.is_empty() { + return Err(Error::InvalidCommandArguments( + "SETEVENTS event codes list mut not be empty".to_string(), + )); + } + let command = format!("SETEVENTS {}", event_codes.join(" ")); + + self.write_command(&command) + } + + // AUTHENTICATE (3.5) + fn authenticate_cmd(&mut self, password: &str) -> Result { + let command = format!("AUTHENTICATE \"{}\"", quoted_string(password)); + + self.write_command(&command) + } + + // AUTHENTICATE (3.5) + fn authenticate_cmd_cookie(&mut self, cookie: &[u8]) -> Result { + let mut command = b"AUTHENTICATE "[..].to_owned(); + for b in cookie { + write!(&mut command, "{:02x}", b).map_err(|e| Error::InvalidCommandArguments(e.to_string()))?; + } + + self.write_command(unsafe { str::from_utf8_unchecked(&command) }) + } + + // AUTHCHALLENGE (3.24) + fn authchallenge_cmd(&mut self, client_nonce: &[u8]) -> Result { + let mut command = b"AUTHCHALLENGE SAFECOOKIE "[..].to_owned(); + for b in client_nonce { + write!(&mut command, "{:02x}", b).map_err(|e| Error::InvalidCommandArguments(e.to_string()))?; + } + + self.write_command(unsafe { str::from_utf8_unchecked(&command) }) + } + + // PROTOCOLINFO (3.21) + fn protocolinfo_cmd(&mut self) -> Result { + self.write_command("PROTOCOLINFO 1") + } + + // GETINFO (3.9) + fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result { + if keywords.is_empty() { + return Err(Error::InvalidCommandArguments( + "GETINFO keywords list must not be empty".to_string(), + )); + } + let command = format!("GETINFO {}", keywords.join(" ")); + + self.write_command(&command) + } + + // ADD_ONION (3.27) + fn add_onion_cmd( + &mut self, + key: Option<&Ed25519PrivateKey>, + flags: &AddOnionFlags, + max_streams: Option, + virt_port: u16, + target: Option, + client_auth: Option<&[X25519PublicKey]>, + ) -> Result { + let mut command_buffer = vec!["ADD_ONION".to_string()]; + + // set our key or request a new one + if let Some(key) = key { + command_buffer.push(key.to_key_blob()); + } else { + command_buffer.push("NEW:ED25519-V3".to_string()); + } + + // set our flags + let mut flag_buffer: Vec<&str> = Default::default(); + if flags.discard_pk { + flag_buffer.push("DiscardPK"); + } + if flags.detach { + flag_buffer.push("Detach"); + } + if flags.v3_auth { + flag_buffer.push("V3Auth"); + } + if flags.non_anonymous { + flag_buffer.push("NonAnonymous"); + } + if flags.max_streams_close_circuit { + flag_buffer.push("MaxStreamsCloseCircuit"); + } + + if !flag_buffer.is_empty() { + command_buffer.push(format!("Flags={}", flag_buffer.join(","))); + } + + // set max concurrent streams + if let Some(max_streams) = max_streams { + command_buffer.push(format!("MaxStreams={}", max_streams)); + } + + // set our onion service target + if let Some(target) = target { + command_buffer.push(format!("Port={},{}", virt_port, target)); + } else { + command_buffer.push(format!("Port={}", virt_port)); + } + // setup client auth + if let Some(client_auth) = client_auth { + for key in client_auth.iter() { + command_buffer.push(format!("ClientAuthV3={}", key.to_base32())); + } + } + + // finally send the command + let command = command_buffer.join(" "); + + self.write_command(&command) + } + + // DEL_ONION (3.38) + fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result { + let command = format!("DEL_ONION {}", service_id); + + self.write_command(&command) + } + + // ONION_CLIENT_AUTH_ADD (3.30) + fn onion_client_auth_add_cmd( + &mut self, + service_id: &V3OnionServiceId, + private_key: &X25519PrivateKey, + client_name: Option, + flags: &OnionClientAuthAddFlags, + ) -> Result { + let mut command_buffer = vec!["ONION_CLIENT_AUTH_ADD".to_string()]; + + // set the onion service id + command_buffer.push(service_id.to_string()); + + // set our client's private key + command_buffer.push(format!("x25519:{}", private_key.to_base64())); + + if let Some(client_name) = client_name { + command_buffer.push(format!("ClientName={}", client_name)); + } + + if flags.permanent { + command_buffer.push("Flags=Permanent".to_string()); + } + + // finally send command + let command = command_buffer.join(" "); + + self.write_command(&command) + } + + // ONION_CLIENT_AUTH_REMOVE (3.31) + fn onion_client_auth_remove_cmd( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result { + let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id); + + self.write_command(&command) + } + + // + // Public high-level typesafe command method wrappers + // + + pub fn setconf(&mut self, key_values: &[(&str, String)]) -> Result<(), Error> { + self.setconf_cmd(key_values).and_then(reply_ok).map(|_| ()) + } + + #[cfg(test)] + pub fn getconf(&mut self, keywords: &[&str]) -> Result, Error> { + let reply = self.getconf_cmd(keywords).and_then(reply_ok)?; + + let mut key_values: Vec<(String, String)> = Default::default(); + for line in reply.reply_lines { + match line.find('=') { + Some(index) => key_values + .push((line[0..index].to_string(), line[index + 1..].to_string())), + None => key_values.push((line, String::new())), + } + } + Ok(key_values) + } + + pub fn setevents(&mut self, events: &[&str]) -> Result<(), Error> { + self.setevents_cmd(events).and_then(reply_ok).map(|_| ()) + } + + pub fn authenticate(&mut self, password: &str) -> Result<(), Error> { + self.authenticate_cmd(password).and_then(reply_ok).map(|_| ()) + } + + fn ensure_protocolinfo(&mut self) { + if self.protocolinfo_data.is_some() { + return; + } + + // https://raw.githubusercontent.com/torproject/torspec/refs/heads/main/control-spec.txt + // 250-VERSION Tor=\"0.4.7.16\" + match self.protocolinfo_cmd() { + Ok(reply) if reply.status_code == 250 => { + if let Some(vers) = reply.reply_lines.iter().find(|l| l.starts_with("VERSION Tor=\"")) { + self.version = vers["VERSION Tor=\"".len()..].split('\"').next().and_then(|s| <_>::from_str(s).ok()); + } + + self.protocolinfo_data = Some(reply.reply_lines.iter() + .find(|l| l.starts_with("AUTH METHODS=")) + .map(|auth| parse_auth_methods(auth)) + .unwrap_or_default()); + } + _ => self.protocolinfo_data = Some(Default::default()), + } + } + + fn authenticate_safecookie(&mut self, data: [u8; 32]) -> Result { + let mut client_nonce = [0u8; 32]; + OsRng.try_fill_bytes(&mut client_nonce).map_err(Error::RngError)?; + let reply = self.authchallenge_cmd(&client_nonce).and_then(reply_ok)?; + + if reply.reply_lines.len() != 1 || !reply.reply_lines[0].starts_with("AUTHCHALLENGE SERVERHASH=") { + return Err(Error::CommandReplyParseFailed(reply.reply_lines.get(0).cloned().unwrap_or_else(|| "[no response]".to_string()))); + } + let mut chunks = reply.reply_lines[0]["AUTHCHALLENGE SERVERHASH=".len()..].splitn(2, ' '); + + let sh = chunks.next().map(|sh| sh.as_bytes()) + .filter(|sh| sh.len() % 64 == 0) + .filter(|sh| sh.iter().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F'))) + .ok_or_else(|| Error::CommandReplyParseFailed(reply.reply_lines[0].clone()))?; + let mut server_hash = Vec::new(); + server_hash.resize(sh.len() / 2, 0); + for (hilo, dest) in sh.chunks_exact(2).zip(server_hash.iter_mut()) { + *dest = tonibble(hilo[0]) << 4 | tonibble(hilo[1]); + } + + let sn = chunks.next().map(|sh| sh.as_bytes()) + .filter(|sn| sn.starts_with(b"SERVERNONCE=")) + .map(|sn| &sn[b"SERVERNONCE=".len()..]) + .filter(|sh| sh.len() == 64) + .filter(|sh| sh.iter().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F'))) + .ok_or_else(|| Error::CommandReplyParseFailed(reply.reply_lines[0].clone()))?; + let mut server_nonce = [0u8; 32]; + for (hilo, dest) in sn.chunks_exact(2).zip(server_nonce.iter_mut()) { + *dest = tonibble(hilo[0]) << 4 | tonibble(hilo[1]); + } + + hmac_sha256("Tor safe cookie authentication server-to-controller hash", &data, &client_nonce, &server_nonce) + .verify_slice(&server_hash).map_err(|_| Error::BadCookieHash())?; + + self.authenticate_cmd_cookie( + hmac_sha256("Tor safe cookie authentication controller-to-server hash", &data, &client_nonce, &server_nonce).finalize().into_bytes().as_slice()) + } + + pub fn authenticate_cookie(&mut self, data: [u8; 32]) -> Result<(), Error> { + self.ensure_protocolinfo(); + let Some(pi) = self.protocolinfo_data.as_ref() + else { unreachable!() }; + + let reply = if pi.auth_safecookie && pi.auth_cookie { + match self.authenticate_safecookie(data) { + r @ Ok(_) | r @ Err(Error::BadCookieHash()) => r?, + _ => self.authenticate_cmd_cookie(&data)?, + } + } else if pi.auth_safecookie { + self.authenticate_safecookie(data)? + } else if pi.auth_cookie { + self.authenticate_cmd_cookie(&data)? + } else { + return Err(Error::CookiesNotSupported()); + }; + + reply_ok(reply).map(|_| ()) + } + + pub fn authenticate_auto(&mut self) -> Result<(), Error> { + self.ensure_protocolinfo(); + let Some(pi) = self.protocolinfo_data.as_ref() + else { unreachable!() }; + + if pi.auth_null { + self.authenticate("") + } else if (pi.auth_cookie || pi.auth_safecookie) && pi.cookiefile != Path::new("") { + self.authenticate_cookie(read_cookie(&pi.cookiefile).map_err(|e| Error::CookieReadingFailed(e, pi.cookiefile.clone()))?) + } else { + self.authenticate("") // fallback + } + } + + pub fn getinfo(&mut self, keywords: &[&str]) -> Result, Error> { + let reply = self.getinfo_cmd(keywords).and_then(reply_ok)?; + + let mut key_values: Vec<(String, String)> = Default::default(); + for line in reply.reply_lines { + match line.find('=') { + Some(index) => key_values + .push((line[0..index].to_string(), line[index + 1..].to_string())), + None => { + if line != "OK" { + key_values.push((line, String::new())) + } + } + } + } + Ok(key_values) + } + + pub fn add_onion( + &mut self, + key: Option<&Ed25519PrivateKey>, + flags: &AddOnionFlags, + max_streams: Option, + virt_port: u16, + target: Option, + client_auth: Option<&[X25519PublicKey]>, + ) -> Result<(Option, V3OnionServiceId), Error> { + let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth).and_then(reply_ok)?; + + let mut private_key: Option = None; + let mut service_id: Option = None; + + for line in reply.reply_lines { + if let Some(mut index) = line.find("ServiceID=") { + if service_id.is_some() { + return Err(Error::CommandReplyParseFailed( + "received duplicate ServiceID entries".to_string(), + )); + } + index += "ServiceId=".len(); + let service_id_string = &line[index..]; + service_id = match V3OnionServiceId::from_string(service_id_string) { + Ok(service_id) => Some(service_id), + Err(_) => { + return Err(Error::CommandReplyParseFailed(format!( + "could not parse '{}' as V3OnionServiceId", + service_id_string + ))) + } + } + } else if let Some(mut index) = line.find("PrivateKey=") { + if private_key.is_some() { + return Err(Error::CommandReplyParseFailed( + "received duplicate PrivateKey entries".to_string(), + )); + } + index += "PrivateKey=".len(); + let key_blob_string = &line[index..]; + private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string) + { + Ok(private_key) => Some(private_key), + Err(_) => { + return Err(Error::CommandReplyParseFailed(format!( + "could not parse {} as Ed25519PrivateKey", + key_blob_string + ))) + } + }; + } else if line.contains("ClientAuthV3=") { + if client_auth.unwrap_or_default().is_empty() { + return Err(Error::CommandReplyParseFailed( + "recieved unexpected ClientAuthV3 keys".to_string(), + )); + } + } else if !line.contains("OK") { + return Err(Error::CommandReplyParseFailed(format!( + "received unexpected reply line '{}'", + line + ))); + } + } + + if flags.discard_pk { + if private_key.is_some() { + return Err(Error::CommandReplyParseFailed( + "PrivateKey response should have been discard".to_string(), + )); + } + } else if private_key.is_none() { + return Err(Error::CommandReplyParseFailed( + "did not receive a PrivateKey".to_string(), + )); + } + + match service_id { + Some(service_id) => Ok((private_key, service_id)), + None => Err(Error::CommandReplyParseFailed( + "did not receive a ServiceID".to_string(), + )), + } + } + + pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { + self.del_onion_cmd(service_id).and_then(reply_ok).map(|_| ()) + } + + // more specific encapulsation of specific command invocations + + pub fn getinfo_net_listeners_socks(&mut self) -> Result, Error> { + let response = self.getinfo(&["net/listeners/socks"])?; + for (key, value) in response.iter() { + if key.as_str() == "net/listeners/socks" { + if value.is_empty() { + return Ok(Default::default()); + } + // get our list of double-quoted strings + let listeners: Vec<&str> = value.split(' ').collect(); + let mut result: Vec = Default::default(); + for socket_addr in listeners.iter() { + if !socket_addr.starts_with('\"') || !socket_addr.ends_with('\"') { + return Err(Error::CommandReplyParseFailed(format!( + "could not parse '{}' as socket address", + socket_addr + ))); + } + + // remove leading/trailing double quote + let stripped = &socket_addr[1..socket_addr.len() - 1]; + result.push(match SocketAddr::from_str(stripped) { + Ok(result) => result, + Err(_) => { + return Err(Error::CommandReplyParseFailed(format!( + "could not parse '{}' as socket address", + socket_addr + ))) + } + }); + } + return Ok(result); + } + } + Err(Error::CommandReplyParseFailed( + "reply did not find a 'net/listeners/socks' key/value".to_string(), + )) + } + + pub fn getinfo_version(&mut self) -> Result { + if let Some(vers) = self.version.take() { + return Ok(vers); + } + + let response = self.getinfo(&["version"])?; + for (key, value) in response.iter() { + if key.as_str() == "version" { + return LegacyTorVersion::from_str(value).map_err(Error::TorVersionParseFailed); + } + } + Err(Error::CommandReplyParseFailed( + "did not find a 'version' key/value".to_string(), + )) + } + + pub fn onion_client_auth_add( + &mut self, + service_id: &V3OnionServiceId, + private_key: &X25519PrivateKey, + client_name: Option, + flags: &OnionClientAuthAddFlags, + ) -> Result<(), Error> { + let reply = self.onion_client_auth_add_cmd(service_id, private_key, client_name, flags)?; + + match reply.status_code { + 250u32..=252u32 => Ok(()), + code => Err(Error::CommandFailed(code, reply.reply_lines)), + } + } + + #[allow(dead_code)] + pub fn onion_client_auth_remove(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { + let reply = self.onion_client_auth_remove_cmd(service_id)?; + + match reply.status_code { + 250u32..=251u32 => Ok(()), + code => Err(Error::CommandFailed(code, reply.reply_lines)), + } + } +} + +#[test] +#[serial] +fn test_tor_controller() -> anyhow::Result<()> { + test_tor_controller_impl(false) +} +#[test] +#[serial] +#[cfg(unix)] +fn test_tor_controller_unix() -> anyhow::Result<()> { + test_tor_controller_impl(true) +} +#[cfg(test)] +fn test_tor_controller_impl(unix: bool) -> anyhow::Result<()> { + use std::borrow::Cow; + + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push("test_tor_controller"); + let tor_process = LegacyTorProcess::new_unix(&tor_path, &data_path, unix)?; + + // create a scope to ensure tor_controller is dropped + { + let control_stream = + LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; + + // create a tor controller and send authentication command + let mut tor_controller = LegacyTorController::new(control_stream)?; + tor_controller.authenticate_cmd(tor_process.get_password())?; + assert_eq!( + tor_controller + .authenticate_cmd("invalid password")? + .status_code, + 515u32 + ); + + // tor controller should have shutdown the connection after failed authentication + assert!( + tor_controller + .authenticate_cmd(tor_process.get_password()) + .is_err(), + "expected failure due to closed connection" + ); + assert!(tor_controller.control_stream.closed_by_remote()); + } + // now create a second controller + { + let control_stream = + LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?; + + // create a tor controller and send authentication command + // all async events are just printed to stdout + let mut tor_controller = LegacyTorController::new(control_stream)?; + tor_controller.authenticate(tor_process.get_password())?; + + // ensure everything is matching our default_torrc settings + let vals = tor_controller.getconf(&["SocksPort", "AvoidDiskWrites", "DisableNetwork"])?; + for (key, value) in vals.iter() { + let expected = match key.as_str() { + "SocksPort" => "auto", + "AvoidDiskWrites" => "1", + "DisableNetwork" => "1", + _ => panic!("unexpected returned key: {}", key), + }; + assert_eq!(value, expected); + } + + let vals = tor_controller.getinfo(&["version", "config-file", "config-text"])?; + let mut expected_torrc_path = data_path.clone(); + expected_torrc_path.push("torrc"); + let mut expected_control_port_path = data_path.clone(); + expected_control_port_path.push("control_port"); + for (key, value) in vals.iter() { + match key.as_str() { + "version" => assert!(Regex::new(r"\d+\.\d+\.\d+\.\d+")?.is_match(&value)), + "config-file" => assert_eq!(Path::new(&value), expected_torrc_path), + "config-text" => assert_eq!( + value.to_string(), + format!( + "\nControlPort {}\nControlPortWriteToFile {}\nDataDirectory {}", + if unix { Cow::Owned(format!("unix:{}", expected_control_port_path.with_file_name("control.sock").display())) } else { Cow::Borrowed("auto") }, + expected_control_port_path.display(), + data_path.display() + ) + ), + _ => panic!("unexpected returned key: {}", key), + } + } + + tor_controller.setevents(&["STATUS_CLIENT"])?; + // begin bootstrap + tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?; + + // add an onoin service + let (private_key, service_id) = + match tor_controller.add_onion(None, &Default::default(), None, 22, None, None)? { + (Some(private_key), service_id) => (private_key, service_id), + _ => panic!("add_onion did not return expected values"), + }; + println!("private_key: {}", private_key.to_key_blob()); + println!("service_id: {}", service_id.to_string()); + + assert!( + tor_controller + .del_onion(&V3OnionServiceId::from_string( + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd" + )?) + .is_err(), + "deleting unknown onion should have failed" + ); + + // delete our new onion + tor_controller.del_onion(&service_id)?; + + println!("listeners: "); + for sock_addr in tor_controller.getinfo_net_listeners_socks()?.iter() { + println!(" {}", sock_addr); + } + + // print our event names available to tor + for (key, value) in tor_controller.getinfo(&["events/names"])?.iter() { + println!("{} : {}", key, value); + } + + let stop_time = Instant::now() + std::time::Duration::from_secs(5); + while stop_time > Instant::now() { + for async_event in tor_controller.wait_async_events()?.iter() { + match async_event { + AsyncEvent::Unknown { lines } => { + println!("Unknown: {}", lines.join("\n")); + } + AsyncEvent::StatusClient { + severity, + action, + arguments, + } => { + println!("STATUS_CLIENT severity={}, action={}", severity, action); + for (key, value) in arguments.iter() { + println!(" {}='{}'", key, value); + } + } + AsyncEvent::HsDesc { action, hs_address } => { + println!( + "HS_DESC action={}, hsaddress={}", + action, + hs_address.to_string() + ); + } + } + } + } + } + Ok(()) +} diff --git a/tor-interface/src/legacy_tor_process.rs b/tor-interface/src/legacy_tor_process.rs new file mode 100644 index 000000000..08ff9ae23 --- /dev/null +++ b/tor-interface/src/legacy_tor_process.rs @@ -0,0 +1,393 @@ +// standard +use std::borrow::Cow; +use std::default::Default; +use std::fs; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::SocketAddr; +use std::ops::Drop; +use std::path::Path; +use std::process; +use std::process::{Child, ChildStdout, Command, Stdio}; +use std::str::FromStr; +use std::string::ToString; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +#[cfg(unix)] +use std::os::unix::net::SocketAddr as UnixSocketAddr; + +// extern crates +use data_encoding::HEXUPPER; +use rand::RngCore; +use sha1::{Digest, Sha1}; +use socks::SocketAddrOrUnixSocketAddr; + +// internal crates +use crate::tor_crypto::generate_password; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("failed to read control port file")] + ControlPortFileReadFailed(#[source] std::io::Error), + + #[error("provided control port file '{0}' larger than expected ({1} bytes)")] + ControlPortFileTooLarge(String, u64), + + #[error("failed to parse '{0}' as control port file")] + ControlPortFileContentsInvalid(String), + + #[error("provided tor bin path '{0}' must be an absolute path")] + TorBinPathNotAbsolute(String), + + #[error("provided data directory '{0}' must be an absolute path")] + TorDataDirectoryPathNotAbsolute(String), + + #[error("failed to create data directory")] + DataDirectoryCreationFailed(#[source] std::io::Error), + + #[error("file exists in provided data directory path '{0}'")] + DataDirectoryPathExistsAsFile(String), + + #[error("failed to create default_torrc file")] + DefaultTorrcFileCreationFailed(#[source] std::io::Error), + + #[error("failed to write default_torrc file")] + DefaultTorrcFileWriteFailed(#[source] std::io::Error), + + #[error("failed to create torrc file")] + TorrcFileCreationFailed(#[source] std::io::Error), + + #[error("failed to remove control_port file")] + ControlPortFileDeleteFailed(#[source] std::io::Error), + + #[error("failed to start legacy tor process")] + LegacyTorProcessStartFailed(#[source] std::io::Error), + + #[error("failed to read control addr from control_file '{0}'")] + ControlPortFileMissing(String), + + #[error("unable to take legacy tor process stdout")] + LegacyTorProcessStdoutTakeFailed(), + + #[error("failed to spawn tor process stdout read thread")] + StdoutReadThreadSpawnFailed(#[source] std::io::Error), +} + +fn read_control_port_file(control_port_file: &Path) -> Result { + // open file + let mut file = File::open(control_port_file).map_err(Error::ControlPortFileReadFailed)?; + + // bail if the file is larger than expected + let metadata = file.metadata().map_err(Error::ControlPortFileReadFailed)?; + if metadata.len() >= 1024 { + return Err(Error::ControlPortFileTooLarge( + format!("{}", control_port_file.display()), + metadata.len(), + )); + } + + // read contents to string + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(Error::ControlPortFileReadFailed)?; + + if contents.starts_with("PORT=") { + let addr_string = &contents.trim_end()["PORT=".len()..]; + if let Ok(addr) = SocketAddr::from_str(addr_string) { + return Ok(addr.into()); + } + } + #[cfg(unix)] + if contents.starts_with("UNIX_PORT=") { + let addr_string = &contents.trim_end()["UNIX_PORT=".len()..]; + if let Ok(addr) = UnixSocketAddr::from_pathname(addr_string) { + return Ok(addr.into()); + } + } + Err(Error::ControlPortFileContentsInvalid(format!( + "{}", + control_port_file.display() + ))) +} + +// Encapsulates the tor daemon process +pub(crate) struct LegacyTorProcess { + control_addr: SocketAddrOrUnixSocketAddr, + process: Child, + password: String, + // stdout data + stdout_lines: Arc>>, +} + +impl LegacyTorProcess { + const S2K_RFC2440_SPECIFIER_LEN: usize = 9; + + fn hash_tor_password_with_salt( + salt: &[u8; Self::S2K_RFC2440_SPECIFIER_LEN], + password: &str, + ) -> String { + assert_eq!(salt[Self::S2K_RFC2440_SPECIFIER_LEN - 1], 0x60); + + // tor-specific rfc 2440 constants + const EXPBIAS: u8 = 6u8; + const C: u8 = 0x60; // salt[S2K_RFC2440_SPECIFIER_LEN - 1] + const COUNT: usize = (16usize + ((C & 15u8) as usize)) << ((C >> 4) + EXPBIAS); + + // squash together our hash input + let mut input: Vec = Default::default(); + // append salt (sans the 'C' constant') + input.extend_from_slice(&salt[0..Self::S2K_RFC2440_SPECIFIER_LEN - 1]); + // append password bytes + input.extend_from_slice(password.as_bytes()); + + let input = input.as_slice(); + let input_len = input.len(); + + let mut sha1 = Sha1::new(); + let mut count = COUNT; + while count > 0 { + if count > input_len { + sha1.update(input); + count -= input_len; + } else { + sha1.update(&input[0..count]); + break; + } + } + + let key = sha1.finalize(); + + let mut hash = "16:".to_string(); + HEXUPPER.encode_append(salt, &mut hash); + HEXUPPER.encode_append(&key, &mut hash); + + hash + } + + fn hash_tor_password(password: &str) -> String { + let mut salt = [0x00u8; Self::S2K_RFC2440_SPECIFIER_LEN]; + let csprng = &mut tor_llcrypto::rng::CautiousRng; + csprng.fill_bytes(&mut salt); + salt[Self::S2K_RFC2440_SPECIFIER_LEN - 1] = 0x60u8; + + Self::hash_tor_password_with_salt(&salt, password) + } + + pub fn get_control_addr(&self) -> &SocketAddrOrUnixSocketAddr { + &self.control_addr + } + + pub fn get_password(&self) -> &String { + &self.password + } + + pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result { + Self::new_unix(tor_bin_path, data_directory, false) + } + + /// Unix mode is test/debug only + pub(crate) fn new_unix(tor_bin_path: &Path, data_directory: &Path, unix: bool) -> Result { + if tor_bin_path.is_relative() { + return Err(Error::TorBinPathNotAbsolute(format!( + "{}", + tor_bin_path.display() + ))); + } + if data_directory.is_relative() { + return Err(Error::TorDataDirectoryPathNotAbsolute(format!( + "{}", + data_directory.display() + ))); + } + + // create data directory if it doesn't exist + if !data_directory.exists() { + fs::create_dir_all(data_directory).map_err(Error::DataDirectoryCreationFailed)?; + } else if data_directory.is_file() { + return Err(Error::DataDirectoryPathExistsAsFile(format!( + "{}", + data_directory.display() + ))); + } + + // construct paths to torrc files + let default_torrc = data_directory.join("default_torrc"); + let torrc = data_directory.join("torrc"); + let control_port_file = data_directory.join("control_port"); + + // TODO: should we nuke the existing torrc between runs? Do we want + // users setting custom nonsense in there? + // construct default torrc + // - daemon determines socks port + // - minimize writes to disk + // - start with network disabled by default + if !default_torrc.exists() { + const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto\n\ + AvoidDiskWrites 1\n\ + DisableNetwork 1\n"; + + let mut default_torrc_file = + File::create(&default_torrc).map_err(Error::DefaultTorrcFileCreationFailed)?; + default_torrc_file + .write_all(DEFAULT_TORRC_CONTENT.as_bytes()) + .map_err(Error::DefaultTorrcFileWriteFailed)?; + } + + // create empty torrc for user + if !torrc.exists() { + let _ = File::create(&torrc).map_err(Error::TorrcFileCreationFailed)?; + } + + // remove any existing control_port_file + if control_port_file.exists() { + fs::remove_file(&control_port_file).map_err(Error::ControlPortFileDeleteFailed)?; + } + + const CONTROL_PORT_PASSWORD_LENGTH: usize = 32usize; + let password = generate_password(CONTROL_PORT_PASSWORD_LENGTH); + let password_hash = Self::hash_tor_password(&password); + + let mut process = Command::new(tor_bin_path.as_os_str()) + .stdout(Stdio::piped()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // set working directory to data directory + .current_dir(data_directory) + // point to our above written torrc file + .arg("--defaults-torrc") + .arg(default_torrc) + // location of torrc + .arg("--torrc-file") + .arg(torrc) + // root data directory + .arg("DataDirectory") + .arg(data_directory) + // daemon will assign us a port, and we will + // read it from the control port file + .arg("ControlPort") + .arg(&*if unix { Cow::Owned(format!("unix:{}", data_directory.join("control.sock").display())) } else { Cow::Borrowed("auto") }) + // control port file destination + .arg("ControlPortWriteToFile") + .arg(&control_port_file) + // use password authentication to prevent other apps + // from modifying our daemon's settings + .arg("HashedControlPassword") + .arg(password_hash) + // tor process will shut down after this process shuts down + // to avoid orphaned tor daemon + .arg("__OwningControllerProcess") + .arg(process::id().to_string()) + .spawn() + .map_err(Error::LegacyTorProcessStartFailed)?; + + let mut control_addr = None; + let start = Instant::now(); + + // try and read the control port from the control port file + // or abort after 5 seconds + // TODO: make this timeout configurable? + while control_addr.is_none() && start.elapsed() < Duration::from_secs(5) { + if control_port_file.exists() { + control_addr = Some(read_control_port_file(control_port_file.as_path())?); + fs::remove_file(&control_port_file).map_err(Error::ControlPortFileDeleteFailed)?; + } + } + + let control_addr = match control_addr { + Some(control_addr) => control_addr, + None => { + return Err(Error::ControlPortFileMissing(format!( + "{}", + control_port_file.display() + ))) + } + }; + + let stdout_lines: Arc>> = Default::default(); + + { + let stdout_lines = Arc::downgrade(&stdout_lines); + let stdout = BufReader::new(match process.stdout.take() { + Some(stdout) => stdout, + None => return Err(Error::LegacyTorProcessStdoutTakeFailed()), + }); + + std::thread::Builder::new() + .name("tor_stdout_reader".to_string()) + .spawn(move || { + LegacyTorProcess::read_stdout_task(&stdout_lines, stdout); + }) + .map_err(Error::StdoutReadThreadSpawnFailed)?; + } + + Ok(LegacyTorProcess { + control_addr, + process, + password, + stdout_lines, + }) + } + + fn read_stdout_task( + stdout_lines: &std::sync::Weak>>, + mut stdout: BufReader, + ) { + while let Some(stdout_lines) = stdout_lines.upgrade() { + let mut line = String::default(); + // read line + if stdout.read_line(&mut line).is_ok() { + // remove trailing '\n' + line.pop(); + // then acquire the lock on the line buffer + let mut stdout_lines = match stdout_lines.lock() { + Ok(stdout_lines) => stdout_lines, + Err(_) => unreachable!(), + }; + stdout_lines.push(line); + } + } + } + + pub fn wait_log_lines(&mut self) -> Vec { + let mut lines = match self.stdout_lines.lock() { + Ok(lines) => lines, + Err(_) => unreachable!(), + }; + std::mem::take(&mut lines) + } +} + +impl Drop for LegacyTorProcess { + fn drop(&mut self) { + let _ = self.process.kill(); + } +} + +#[test] +fn test_password_hash() -> Result<(), anyhow::Error> { + let salt1: [u8; LegacyTorProcess::S2K_RFC2440_SPECIFIER_LEN] = [ + 0xbeu8, 0x2au8, 0x25u8, 0x1du8, 0xe6u8, 0x2cu8, 0xb2u8, 0x7au8, 0x60u8, + ]; + let hash1 = LegacyTorProcess::hash_tor_password_with_salt(&salt1, "abcdefghijklmnopqrstuvwxyz"); + assert_eq!( + hash1, + "16:BE2A251DE62CB27A60AC9178A937990E8ED0AB662FA82A5C7DE3EBB23A" + ); + + let salt2: [u8; LegacyTorProcess::S2K_RFC2440_SPECIFIER_LEN] = [ + 0x36u8, 0x73u8, 0x0eu8, 0xefu8, 0xd1u8, 0x8cu8, 0x60u8, 0xd6u8, 0x60u8, + ]; + let hash2 = LegacyTorProcess::hash_tor_password_with_salt(&salt2, "password"); + assert_eq!( + hash2, + "16:36730EEFD18C60D66052E7EA535438761C0928D316EEA56A190C99B50A" + ); + + // ensure same password is hashed to different things + assert_ne!( + LegacyTorProcess::hash_tor_password("password"), + LegacyTorProcess::hash_tor_password("password") + ); + + Ok(()) +} diff --git a/tor-interface/src/legacy_tor_version.rs b/tor-interface/src/legacy_tor_version.rs new file mode 100644 index 000000000..86789ef00 --- /dev/null +++ b/tor-interface/src/legacy_tor_version.rs @@ -0,0 +1,297 @@ +// standard +use std::cmp::Ordering; +use std::option::Option; +use std::str::FromStr; +use std::string::ToString; + +/// `LegacyTorVersion`-specific error type +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("{}", .0)] + ParseError(String), +} + +/// Type representing a legacy c-tor daemon's version number. This version conforms c-tor's [version-spec](https://spec.torproject.org/version-spec.htm). +#[derive(Clone)] +pub struct LegacyTorVersion { + pub(crate) major: u32, + pub(crate) minor: u32, + pub(crate) micro: u32, + pub(crate) patch_level: u32, + pub(crate) status_tag: Option, +} + +impl LegacyTorVersion { + fn status_tag_pattern_is_match(status_tag: &str) -> bool { + if status_tag.is_empty() { + return false; + } + + for c in status_tag.chars() { + if c.is_whitespace() { + return false; + } + } + true + } + + /// Construct a new `LegacyTorVersion` object. + pub fn new( + major: u32, + minor: u32, + micro: u32, + patch_level: Option, + status_tag: Option<&str>, + ) -> Result { + let status_tag = if let Some(status_tag) = status_tag { + if Self::status_tag_pattern_is_match(status_tag) { + Some(status_tag.to_string()) + } else { + return Err(Error::ParseError( + "tor version status tag may not be empty or contain white-space".to_string(), + )); + } + } else { + None + }; + + Ok(LegacyTorVersion { + major, + minor, + micro, + patch_level: patch_level.unwrap_or(0u32), + status_tag, + }) + } +} + +impl FromStr for LegacyTorVersion { + type Err = Error; + + fn from_str(s: &str) -> Result { + // MAJOR.MINOR.MICRO[.PATCHLEVEL][-STATUS_TAG][ (EXTRA_INFO)]* + let mut tokens = s.split(' '); + let (major, minor, micro, patch_level, status_tag) = + if let Some(version_status_tag) = tokens.next() { + let mut tokens = version_status_tag.split('-'); + let (major, minor, micro, patch_level) = if let Some(version) = tokens.next() { + let mut tokens = version.split('.'); + let major: u32 = if let Some(major) = tokens.next() { + match major.parse() { + Ok(major) => major, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as MAJOR portion of tor version", + major + ))) + } + } + } else { + return Err(Error::ParseError( + "failed to find MAJOR portion of tor version".to_string(), + )); + }; + let minor: u32 = if let Some(minor) = tokens.next() { + match minor.parse() { + Ok(minor) => minor, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as MINOR portion of tor version", + minor + ))) + } + } + } else { + return Err(Error::ParseError( + "failed to find MINOR portion of tor version".to_string(), + )); + }; + let micro: u32 = if let Some(micro) = tokens.next() { + match micro.parse() { + Ok(micro) => micro, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as MICRO portion of tor version", + micro + ))) + } + } + } else { + return Err(Error::ParseError( + "failed to find MICRO portion of tor version".to_string(), + )); + }; + let patch_level: u32 = if let Some(patch_level) = tokens.next() { + match patch_level.parse() { + Ok(patch_level) => patch_level, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to parse '{}' as PATCHLEVEL portion of tor version", + patch_level + ))) + } + } + } else { + 0u32 + }; + (major, minor, micro, patch_level) + } else { + // if there were '-' the previous next() would have returned the enire string + unreachable!(); + }; + let status_tag = tokens.next().map(|status_tag| status_tag.to_string()); + + (major, minor, micro, patch_level, status_tag) + } else { + // if there were no ' ' character the previou snext() would have returned the enire string + unreachable!(); + }; + for extra_info in tokens { + if !extra_info.starts_with('(') || !extra_info.ends_with(')') { + return Err(Error::ParseError(format!( + "failed to parse '{}' as [ (EXTRA_INFO)]", + extra_info + ))); + } + } + LegacyTorVersion::new( + major, + minor, + micro, + Some(patch_level), + status_tag.as_deref(), + ) + } +} + +impl ToString for LegacyTorVersion { + fn to_string(&self) -> String { + match &self.status_tag { + Some(status_tag) => format!( + "{}.{}.{}.{}-{}", + self.major, self.minor, self.micro, self.patch_level, status_tag + ), + None => format!( + "{}.{}.{}.{}", + self.major, self.minor, self.micro, self.patch_level + ), + } + } +} + +impl PartialEq for LegacyTorVersion { + fn eq(&self, other: &Self) -> bool { + self.major == other.major + && self.minor == other.minor + && self.micro == other.micro + && self.patch_level == other.patch_level + && self.status_tag == other.status_tag + } +} + +impl PartialOrd for LegacyTorVersion { + fn partial_cmp(&self, other: &Self) -> Option { + if let Some(order) = self.major.partial_cmp(&other.major) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.minor.partial_cmp(&other.minor) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.micro.partial_cmp(&other.micro) { + if order != Ordering::Equal { + return Some(order); + } + } + + if let Some(order) = self.patch_level.partial_cmp(&other.patch_level) { + if order != Ordering::Equal { + return Some(order); + } + } + + // version-spect.txt *does* say that we should compare tags lexicgraphically + // if all of the version numbers are the same when comparing, but we are + // going to diverge here and say we can only compare tags for equality. + // + // In practice we will be comparing tor daemon tags against tagless (stable) + // versions so this shouldn't be an issue + + if self.status_tag == other.status_tag { + return Some(Ordering::Equal); + } + + None + } +} + +#[test] +fn test_version() -> anyhow::Result<()> { + assert!(LegacyTorVersion::from_str("1.2.3")? == LegacyTorVersion::new(1, 2, 3, None, None)?); + assert!( + LegacyTorVersion::from_str("1.2.3.4")? == LegacyTorVersion::new(1, 2, 3, Some(4), None)? + ); + assert!( + LegacyTorVersion::from_str("1.2.3-test")? + == LegacyTorVersion::new(1, 2, 3, None, Some("test"))? + ); + assert!( + LegacyTorVersion::from_str("1.2.3.4-test")? + == LegacyTorVersion::new(1, 2, 3, Some(4), Some("test"))? + ); + assert!( + LegacyTorVersion::from_str("1.2.3 (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, None, None)? + ); + assert!( + LegacyTorVersion::from_str("1.2.3.4 (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, Some(4), None)? + ); + assert!( + LegacyTorVersion::from_str("1.2.3.4-tag (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))? + ); + + assert!( + LegacyTorVersion::from_str("1.2.3.4-tag (extra_info) (extra_info)")? + == LegacyTorVersion::new(1, 2, 3, Some(4), Some("tag"))? + ); + + assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("spaced tag")).is_err()); + assert!(LegacyTorVersion::new(1, 2, 3, Some(4), Some("" /* empty tag */)).is_err()); + assert!(LegacyTorVersion::from_str("").is_err()); + assert!(LegacyTorVersion::from_str("1.2").is_err()); + assert!(LegacyTorVersion::from_str("1.2-foo").is_err()); + assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar").is_err()); + assert!(LegacyTorVersion::from_str("1.2.3.4-foo bar (extra_info)").is_err()); + assert!(LegacyTorVersion::from_str("1.2.3.4-foo (extra_info) badtext").is_err()); + assert!( + LegacyTorVersion::new(0, 0, 0, Some(0), None)? + < LegacyTorVersion::new(1, 0, 0, Some(0), None)? + ); + assert!( + LegacyTorVersion::new(0, 0, 0, Some(0), None)? + < LegacyTorVersion::new(0, 1, 0, Some(0), None)? + ); + assert!( + LegacyTorVersion::new(0, 0, 0, Some(0), None)? + < LegacyTorVersion::new(0, 0, 1, Some(0), None)? + ); + + // ensure status tags make comparison between equal versions (apart from + // tags) unknowable + let zero_version = LegacyTorVersion::new(0, 0, 0, Some(0), None)?; + let zero_version_tag = LegacyTorVersion::new(0, 0, 0, Some(0), Some("tag"))?; + + assert!(!(zero_version < zero_version_tag)); + assert!(!(zero_version <= zero_version_tag)); + assert!(!(zero_version > zero_version_tag)); + assert!(!(zero_version >= zero_version_tag)); + + Ok(()) +} diff --git a/tor-interface/src/lib.rs b/tor-interface/src/lib.rs new file mode 100644 index 000000000..d334e5a79 --- /dev/null +++ b/tor-interface/src/lib.rs @@ -0,0 +1,35 @@ +#![doc = include_str!("../README.md")] + +/// Implementation of an in-process [`arti-client`](https://crates.io/crates/arti-client)-based `TorProvider` +#[cfg(feature = "arti-client-tor-provider")] +pub mod arti_client_tor_client; +/// Implementation of an out-of-process [`arti`](https://crates.io/crates/arti)-based `TorProvider` +#[cfg(feature = "arti-tor-provider")] +pub mod arti_tor_client; +#[cfg(feature = "arti-tor-provider")] +pub mod arti_process; +/// Censorship circumvention configuration for pluggable-transports and bridge settings +#[cfg(feature = "legacy-tor-provider")] +pub mod censorship_circumvention; +/// Implementation of an out-of-process legacy [c-tor daemon](https://gitlab.torproject.org/tpo/core/tor)-based `TorProvider` +#[cfg(feature = "legacy-tor-provider")] +pub mod legacy_tor_client; +#[cfg(feature = "legacy-tor-provider")] +mod legacy_tor_control_stream; +#[cfg(feature = "legacy-tor-provider")] +mod legacy_tor_controller; +#[cfg(feature = "legacy-tor-provider")] +mod legacy_tor_process; +/// Legacy c-tor daemon version. +#[cfg(feature = "legacy-tor-provider")] +pub mod legacy_tor_version; +/// Implementation of a local, in-process, mock `TorProvider` for testing. +#[cfg(feature = "mock-tor-provider")] +pub mod mock_tor_client; +/// Proxy settings +#[cfg(feature = "legacy-tor-provider")] +pub mod proxy; +/// Tor-specific cryptographic primitives, operations, and conversion functions. +pub mod tor_crypto; +/// Traits and types for connecting to the Tor Network. +pub mod tor_provider; diff --git a/tor-interface/src/mock_tor_client.rs b/tor-interface/src/mock_tor_client.rs new file mode 100644 index 000000000..eded7daa3 --- /dev/null +++ b/tor-interface/src/mock_tor_client.rs @@ -0,0 +1,345 @@ +// standard +use std::collections::BTreeMap; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::sync::{atomic, Arc, Mutex}; + +// internal crates +use crate::tor_crypto::*; +use crate::tor_provider; +use crate::tor_provider::*; + + +/// [`MockTorClient`]-specific error type +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("client not bootstrapped")] + ClientNotBootstrapped(), + + #[error("client already bootstrapped")] + ClientAlreadyBootstrapped(), + + #[error("onion service not found: {}", .0)] + OnionServiceNotFound(OnionAddr), + + #[error("onion service not published: {}", .0)] + OnionServiceNotPublished(OnionAddr), + + #[error("onion service requires onion auth")] + OnionServiceRequiresOnionAuth(), + + #[error("provided onion auth key invalid")] + OnionServiceAuthInvalid(), + + #[error("unable to bind TCP listener")] + TcpListenerBindFailed(#[source] std::io::Error), + + #[error("unable to get TCP listener's local adress")] + TcpListenerLocalAddrFailed(#[source] std::io::Error), + + #[error("unable to connect to {}", .0)] + ConnectFailed(TargetAddr), + + #[error("not implemented")] + NotImplemented(), +} + +impl From for crate::tor_provider::Error { + fn from(error: Error) -> Self { + crate::tor_provider::Error::Generic(error.to_string()) + } +} + +struct MockTorNetwork { + onion_services: Option, SocketAddr)>>, +} + +impl MockTorNetwork { + const fn new() -> MockTorNetwork { + MockTorNetwork { + onion_services: None, + } + } + + fn connect_to_onion( + &mut self, + service_id: &V3OnionServiceId, + virt_port: u16, + client_auth: Option<&X25519PublicKey>, + ) -> Result<::Stream, Error> { + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); + + match &mut self.onion_services { + Some(onion_services) => { + if let Some((client_auth_keys, socket_addr)) = onion_services.get(&onion_addr) { + match (client_auth_keys.len(), client_auth) { + (0, None) => (), + (_, None) => return Err(Error::OnionServiceRequiresOnionAuth()), + (0, Some(_)) => return Err(Error::OnionServiceAuthInvalid()), + (_, Some(client_auth)) => { + if !client_auth_keys.contains(client_auth) { + return Err(Error::OnionServiceAuthInvalid()); + } + } + } + + if let Ok(stream) = TcpStream::connect(socket_addr) { + Ok(TcpOrUnixOnionStream { + stream: stream.into(), + local_addr: None, + peer_addr: Some(TargetAddr::OnionService(onion_addr)), + }) + } else { + Err(Error::OnionServiceNotFound(onion_addr)) + } + } else { + Err(Error::OnionServiceNotPublished(onion_addr)) + } + } + None => Err(Error::OnionServiceNotPublished(onion_addr)), + } + } + + fn start_onion( + &mut self, + service_id: V3OnionServiceId, + virt_port: u16, + client_auth_keys: Vec, + address: SocketAddr, + ) { + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id, virt_port)); + match &mut self.onion_services { + Some(onion_services) => { + onion_services.insert(onion_addr, (client_auth_keys, address)); + } + None => { + let mut onion_services = BTreeMap::new(); + onion_services.insert(onion_addr, (client_auth_keys, address)); + self.onion_services = Some(onion_services); + } + } + } + + fn stop_onion(&mut self, onion_addr: &OnionAddr) { + if let Some(onion_services) = &mut self.onion_services { + onion_services.remove(onion_addr); + } + } +} + +static MOCK_TOR_NETWORK: Mutex = Mutex::new(MockTorNetwork::new()); + +/// A mock `TorProvider` implementation for testing. +/// +/// `MockTorClient` implements the [`TorProvider`] trait. It creates a fake, in-process Tor Network using local socekts and listeners. No actual traffic ever leaves the local host. +/// +/// Mock onion-services can be created, connected to, and communiccated with. Connecting to clearnet targets always succeeds by connecting to single local endpoint, but will never send any traffic to connecting clients. +pub struct MockTorClient { + events: Vec, + bootstrapped: bool, + client_auth_keys: BTreeMap, + onion_services: Vec<(OnionAddr, Arc)>, + loopback: TcpListener, +} + +impl MockTorClient { + /// Construct a new `MockTorClient`. + pub fn new() -> MockTorClient { + let mut events: Vec = Default::default(); + let line = "[notice] MockTorClient running".to_string(); + events.push(TorEvent::LogReceived { line }); + + let socket_addr = SocketAddr::from(([127, 0, 0, 1], 0u16)); + let listener = TcpListener::bind(socket_addr).expect("tcplistener bind failed"); + + MockTorClient { + events, + bootstrapped: false, + client_auth_keys: Default::default(), + onion_services: Default::default(), + loopback: listener, + } + } +} + +impl Default for MockTorClient { + fn default() -> Self { + Self::new() + } +} + +impl TorProvider for MockTorClient { + type Stream = TcpOrUnixOnionStream; + type Listener = TcpOnionListener; + + fn update(&mut self) -> Result, tor_provider::Error> { + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => { + let mut i = 0; + while i < self.onion_services.len() { + // remove onion services with no active listeners + if !self.onion_services[i].1.load(atomic::Ordering::Relaxed) { + let entry = self.onion_services.swap_remove(i); + let onion_addr = entry.0; + mock_tor_network.stop_onion(&onion_addr); + } else { + i += 1; + } + } + } + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } + + Ok(std::mem::take(&mut self.events)) + } + + fn bootstrap(&mut self) -> Result<(), tor_provider::Error> { + if self.bootstrapped { + Err(Error::ClientAlreadyBootstrapped())? + } else { + self.events.push(TorEvent::BootstrapStatus { + progress: 0u32, + tag: "start".to_string(), + summary: "bootstrapping started".to_string(), + }); + self.events.push(TorEvent::BootstrapStatus { + progress: 50u32, + tag: "middle".to_string(), + summary: "bootstrapping continues".to_string(), + }); + self.events.push(TorEvent::BootstrapStatus { + progress: 100u32, + tag: "finished".to_string(), + summary: "bootstrapping completed".to_string(), + }); + self.events.push(TorEvent::BootstrapComplete); + self.bootstrapped = true; + Ok(()) + } + } + + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), tor_provider::Error> { + let client_auth_public = X25519PublicKey::from_private_key(client_auth); + if let Some(key) = self.client_auth_keys.get_mut(service_id) { + *key = client_auth_public; + } else { + self.client_auth_keys + .insert(service_id.clone(), client_auth_public); + } + Ok(()) + } + + fn remove_client_auth( + &mut self, + service_id: &V3OnionServiceId, + ) -> Result<(), tor_provider::Error> { + self.client_auth_keys.remove(service_id); + Ok(()) + } + + fn connect( + &mut self, + target: TargetAddr, + _circuit: Option, + ) -> Result { + let (service_id, virt_port) = match target { + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3 { + service_id, + virt_port, + })) => (service_id, virt_port), + target_address => { + if let Ok(stream) = TcpStream::connect( + self.loopback + .local_addr() + .expect("loopback local_addr failed"), + ) { + return Ok(TcpOrUnixOnionStream { + stream: stream.into(), + local_addr: None, + peer_addr: Some(target_address), + }); + } else { + return Err(Error::ConnectFailed(target_address).into()); + } + } + }; + let client_auth = self.client_auth_keys.get(&service_id); + + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => { + Ok(mock_tor_network.connect_to_onion(&service_id, virt_port, client_auth)?) + } + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } + } + + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorized_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, + ) -> Result { + // convert inputs to relevant types + let service_id = V3OnionServiceId::from_private_key(private_key); + let onion_addr = OnionAddr::V3(OnionAddrV3::new(service_id.clone(), virt_port)); + let authorized_clients: Vec = match authorized_clients { + Some(keys) => keys.into(), + None => Default::default(), + }; + + // try to bind to a local address, let OS pick our port + let socket_addr = bind_addr.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 0u16))); + let listener = TcpListener::bind(socket_addr).map_err(Error::TcpListenerBindFailed)?; + let socket_addr = listener + .local_addr() + .map_err(Error::TcpListenerLocalAddrFailed)?; + + // register the onion service with the mock tor network + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => mock_tor_network.start_onion( + service_id.clone(), + virt_port, + authorized_clients, + socket_addr, + ), + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } + + // init flag for signaling when listener goes out of scope so we can tear down onion service + let is_active = Arc::new(atomic::AtomicBool::new(true)); + self.onion_services + .push((onion_addr.clone(), Arc::clone(&is_active))); + + // onion service published event + self.events + .push(TorEvent::OnionServicePublished { service_id }); + + + Ok(TcpOnionListener(TcpOnionListenerBase(listener, onion_addr), is_active)) + } + + fn generate_token(&mut self) -> CircuitToken { + 0usize + } + + fn release_token(&mut self, _token: CircuitToken) {} +} + +impl Drop for MockTorClient { + fn drop(&mut self) { + // remove all our onion services + match MOCK_TOR_NETWORK.lock() { + Ok(mut mock_tor_network) => { + for entry in self.onion_services.iter() { + let onion_addr = &entry.0; + mock_tor_network.stop_onion(onion_addr); + } + } + Err(_) => unreachable!("another thread panicked while holding mock tor network's lock"), + } + } +} diff --git a/tor-interface/src/proxy.rs b/tor-interface/src/proxy.rs new file mode 100644 index 000000000..b0f84bf6b --- /dev/null +++ b/tor-interface/src/proxy.rs @@ -0,0 +1,163 @@ +// internal crates +use crate::tor_provider::TargetAddr; + +#[derive(thiserror::Error, Debug)] +/// Error type for the proxy module +pub enum ProxyConfigError { + #[error("{0}")] + /// An error returned when constructing a proxy configuration with invalid parameters + Generic(String), +} + +#[derive(Clone, Debug)] +/// Configuration for a SOCKS4 proxy +pub struct Socks4ProxyConfig { + pub(crate) address: TargetAddr, +} + +impl Socks4ProxyConfig { + /// Construct a new `Socks4ProxyConfig`. The `address` argument must not be a [`crate::tor_provider::TargetAddr::OnionService`] and its port must not be 0. + pub fn new(address: TargetAddr) -> Result { + let port = match &address { + TargetAddr::Socket(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + Ok(Self { address }) + } +} + +#[derive(Clone, Debug)] +/// Configuration for a SOCKS5 proxy +pub struct Socks5ProxyConfig { + pub(crate) address: TargetAddr, + pub(crate) username: Option, + pub(crate) password: Option, +} + +impl Socks5ProxyConfig { + /// Construct a new `Socks5ProxyConfig`. The `address` argument must not be a [`crate::tor_provider::TargetAddr::OnionService`] and its port must not be 0. The `username` and `password` arguments, if present, must each be less than 256 bytes long. + pub fn new( + address: TargetAddr, + username: Option, + password: Option, + ) -> Result { + let port = match &address { + TargetAddr::Socket(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + // username must be less than 255 bytes + if let Some(username) = &username { + if username.len() > 255 { + return Err(ProxyConfigError::Generic( + "socks5 username must be <= 255 bytes".to_string(), + )); + } + } + // password must be less than 255 bytes + if let Some(password) = &password { + if password.len() > 255 { + return Err(ProxyConfigError::Generic( + "socks5 password must be <= 255 bytes".to_string(), + )); + } + } + + Ok(Self { + address, + username, + password, + }) + } +} + +#[derive(Clone, Debug)] +/// Configuration for an HTTP CONNECT proxy (`HTTPSProxy` in c-tor torrc configuration) +pub struct HttpsProxyConfig { + pub(crate) address: TargetAddr, + pub(crate) username: Option, + pub(crate) password: Option, +} + +impl HttpsProxyConfig { + /// Construct a new `HttpsProxyConfig`. The `address` argument must not be a [`crate::tor_provider::TargetAddr::OnionService`] and its port must not be 0. The `username` argument, if present, must not contain the `:` (colon) character. + pub fn new( + address: TargetAddr, + username: Option, + password: Option, + ) -> Result { + let port = match &address { + TargetAddr::Socket(addr) => addr.port(), + TargetAddr::Domain(addr) => addr.port(), + TargetAddr::OnionService(_) => { + return Err(ProxyConfigError::Generic( + "proxy address may not be onion service".to_string(), + )) + } + }; + if port == 0 { + return Err(ProxyConfigError::Generic("proxy port not be 0".to_string())); + } + + // username may not contain ':' character (per RFC 2617) + if let Some(username) = &username { + if username.contains(':') { + return Err(ProxyConfigError::Generic( + "username may not contain ':' character".to_string(), + )); + } + } + + Ok(Self { + address, + username, + password, + }) + } +} + +#[derive(Clone, Debug)] +/// An enum representing a possible proxy server configuration with address and possible credentials. +pub enum ProxyConfig { + /// A SOCKS4 proxy + Socks4(Socks4ProxyConfig), + /// A SOCKS5 proxy + Socks5(Socks5ProxyConfig), + /// An HTTP CONNECT proxy + Https(HttpsProxyConfig), +} + +impl From for ProxyConfig { + fn from(config: Socks4ProxyConfig) -> Self { + ProxyConfig::Socks4(config) + } +} + +impl From for ProxyConfig { + fn from(config: Socks5ProxyConfig) -> Self { + ProxyConfig::Socks5(config) + } +} + +impl From for ProxyConfig { + fn from(config: HttpsProxyConfig) -> Self { + ProxyConfig::Https(config) + } +} diff --git a/tor-interface/src/tor_crypto.rs b/tor-interface/src/tor_crypto.rs new file mode 100644 index 000000000..72f7b27fa --- /dev/null +++ b/tor-interface/src/tor_crypto.rs @@ -0,0 +1,821 @@ +// standard +use std::convert::TryInto; +use std::str; + +// extern crates +use curve25519_dalek::Scalar; +use data_encoding::{BASE32_NOPAD, BASE64}; +use data_encoding_macro::new_encoding; +#[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] +use rand::distr::Alphanumeric; +#[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] +use rand::Rng; +use sha3::{Digest, Sha3_256}; +use static_assertions::const_assert_eq; +use tor_llcrypto::pk::keymanip::*; +use tor_llcrypto::*; + +/// Represents various errors that can occur in the tor_crypto module. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// A error encountered converting a String to a tor_crypto type + #[error("{0}")] + ParseError(String), + + /// An error encountered converting between tor_crypto types + #[error("{0}")] + ConversionError(String), + + /// An error encountered converting from a raw byte representation + #[error("invalid key")] + KeyInvalid, +} + +/// The number of bytes in an ed25519 secret key +/// cbindgen:ignore +pub const ED25519_PRIVATE_KEY_SIZE: usize = 64; +/// The number of bytes in an ed25519 public key +/// cbindgen:ignore +pub const ED25519_PUBLIC_KEY_SIZE: usize = 32; +/// The number of bytes in an ed25519 signature +/// cbindgen:ignore +pub const ED25519_SIGNATURE_SIZE: usize = 64; +/// The number of bytes needed to store onion service id as an ASCII c-string (not including null-terminator) +pub const V3_ONION_SERVICE_ID_STRING_LENGTH: usize = 56; +/// The number of bytes needed to store onion service id as an ASCII c-string (including null-terminator) +pub const V3_ONION_SERVICE_ID_STRING_SIZE: usize = 57; +const_assert_eq!( + V3_ONION_SERVICE_ID_STRING_SIZE, + V3_ONION_SERVICE_ID_STRING_LENGTH + 1 +); +/// The number of bytes needed to store base64 encoded ed25519 private key as an ASCII c-string (not including null-terminator) +pub const ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH: usize = 88; +/// key klob header string +const ED25519_PRIVATE_KEY_KEYBLOB_HEADER: &str = "ED25519-V3:"; +/// The number of bytes needed to store the keyblob header +pub const ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH: usize = 11; +const_assert_eq!( + ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH, + ED25519_PRIVATE_KEY_KEYBLOB_HEADER.len() +); +/// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (not including a null terminator) +pub const ED25519_PRIVATE_KEY_KEYBLOB_LENGTH: usize = 99; +const_assert_eq!( + ED25519_PRIVATE_KEY_KEYBLOB_LENGTH, + ED25519_PRIVATE_KEY_KEYBLOB_HEADER_LENGTH + ED25519_PRIVATE_KEYBLOB_BASE64_LENGTH +); +/// The number of bytes needed to store ed25519 private keyblob as an ASCII c-string (including a null terminator) +pub const ED25519_PRIVATE_KEY_KEYBLOB_SIZE: usize = 100; +const_assert_eq!( + ED25519_PRIVATE_KEY_KEYBLOB_SIZE, + ED25519_PRIVATE_KEY_KEYBLOB_LENGTH + 1 +); +// number of bytes in an onion service id after base32 decode +const V3_ONION_SERVICE_ID_RAW_SIZE: usize = 35; +// byte index of the start of the public key checksum +const V3_ONION_SERVICE_ID_CHECKSUM_OFFSET: usize = 32; +// byte index of the v3 onion service version +const V3_ONION_SERVICE_ID_VERSION_OFFSET: usize = 34; +/// The number of bytes in a v3 service id's truncated checksum +const TRUNCATED_CHECKSUM_SIZE: usize = 2; +/// The number of bytes in an x25519 private key +/// cbindgen:ignore +pub const X25519_PRIVATE_KEY_SIZE: usize = 32; +/// The number of bytes in an x25519 publickey +/// cbindgen:ignore +pub const X25519_PUBLIC_KEY_SIZE: usize = 32; +/// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (not including null-terminator) +pub const X25519_PRIVATE_KEY_BASE64_LENGTH: usize = 44; +/// The number of bytes needed to store base64 encoded x25519 private key as an ASCII c-string (including a null terminator) +pub const X25519_PRIVATE_KEY_BASE64_SIZE: usize = 45; +const_assert_eq!( + X25519_PRIVATE_KEY_BASE64_SIZE, + X25519_PRIVATE_KEY_BASE64_LENGTH + 1 +); +/// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (not including null-terminator) +pub const X25519_PUBLIC_KEY_BASE32_LENGTH: usize = 52; +/// The number of bytes needed to store base32 encoded x25519 public key as an ASCII c-string (including a null terminator) +pub const X25519_PUBLIC_KEY_BASE32_SIZE: usize = 53; +const_assert_eq!( + X25519_PUBLIC_KEY_BASE32_SIZE, + X25519_PUBLIC_KEY_BASE32_LENGTH + 1 +); + +const ONION_BASE32: data_encoding::Encoding = new_encoding! { + symbols: "abcdefghijklmnopqrstuvwxyz234567", + padding: '=', +}; + +// Free functions + +// securely generate password using CautionsRng +#[cfg(any(feature = "legacy-tor-provider", feature = "arti-tor-provider"))] +pub(crate) fn generate_password(length: usize) -> String { + let password: String = std::iter::repeat(()) + .map(|()| tor_llcrypto::rng::CautiousRng.sample(Alphanumeric)) + .map(char::from) + .take(length) + .collect(); + + password +} + +// Struct deinitions + +/// An ed25519 private key. +/// +/// This key type is used with [`crate::tor_provider::TorProvider`] trait for hosting onion-services and can be convertd to an [`Ed25519PublicKey`]. It can also be used to sign messages and create an [`Ed25519Signature`]. +pub struct Ed25519PrivateKey { + expanded_keypair: pk::ed25519::ExpandedKeypair, +} + +/// An ed25519 public key. +/// +/// This key type is derived from [`Ed25519PrivateKey`] and can be converted to a [`V3OnionServiceId`]. It can also be used to verify a [`Ed25519Signature`]. +#[derive(Clone)] +pub struct Ed25519PublicKey { + public_key: pk::ed25519::PublicKey, +} + +/// An ed25519 cryptographic signature +#[derive(Clone)] +pub struct Ed25519Signature { + signature: pk::ed25519::Signature, +} + +/// An x25519 private key +#[derive(Clone)] +pub struct X25519PrivateKey { + secret_key: pk::curve25519::StaticSecret, +} + +/// An x25519 public key +#[derive(Clone, PartialEq, Eq)] +pub struct X25519PublicKey { + public_key: pk::curve25519::PublicKey, +} + +/// A v3 onion-service id +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct V3OnionServiceId { + data: [u8; V3_ONION_SERVICE_ID_STRING_LENGTH], +} + +/// An enum representing a single bit +#[derive(Clone, Copy)] +pub enum SignBit { + Zero, + One, +} + +impl From for u8 { + fn from(signbit: SignBit) -> Self { + match signbit { + SignBit::Zero => 0u8, + SignBit::One => 1u8, + } + } +} + +impl From for bool { + fn from(signbit: SignBit) -> Self { + match signbit { + SignBit::Zero => false, + SignBit::One => true, + } + } +} + +impl From for SignBit { + fn from(signbit: bool) -> Self { + if signbit { + SignBit::One + } else { + SignBit::Zero + } + } +} + +// which validation method to use when constructing an ed25519 expanded key from +// a byte array +enum FromRawValidationMethod { + // expanded ed25519 keys coming from legacy c-tor daemon; the scalar portion + // is clamped, but not reduced + #[cfg(feature = "legacy-tor-provider")] + LegacyCTor, + // expanded ed25519 keys coming from ed25519-dalek crate; the scalar portion + // has been clamped AND reduced + Ed25519Dalek, +} + +/// A wrapper around `tor_llcrypto::pk::ed25519::ExpandedKeypair`. +impl Ed25519PrivateKey { + /// Securely generate a new `Ed25519PrivateKey`. + pub fn generate() -> Ed25519PrivateKey { + let csprng = &mut tor_llcrypto::rng::CautiousRng; + let keypair = pk::ed25519::Keypair::generate(csprng); + + Ed25519PrivateKey { + expanded_keypair: pk::ed25519::ExpandedKeypair::from(&keypair), + } + } + + fn from_raw_impl( + raw: &[u8; ED25519_PRIVATE_KEY_SIZE], + method: FromRawValidationMethod, + ) -> Result { + // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1343 + match method { + #[cfg(feature = "legacy-tor-provider")] + FromRawValidationMethod::LegacyCTor => { + // Verify the scalar portion of the expanded key has been clamped + // see: https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 + if !(raw[0] == raw[0] & 248 && raw[31] == (raw[31] & 63) | 64) { + return Err(Error::KeyInvalid); + } + } + FromRawValidationMethod::Ed25519Dalek => { + // Verify the scalar is non-zero and it has been reduced + let scalar: [u8; 32] = raw[..32].try_into().unwrap(); + if scalar.iter().all(|&x| x == 0x00u8) { + return Err(Error::KeyInvalid); + } + let reduced_scalar = Scalar::from_bytes_mod_order(scalar).to_bytes(); + if scalar != reduced_scalar { + return Err(Error::KeyInvalid); + } + } + } + + if let Some(expanded_keypair) = pk::ed25519::ExpandedKeypair::from_secret_key_bytes(*raw) { + Ok(Ed25519PrivateKey { expanded_keypair }) + } else { + Err(Error::KeyInvalid) + } + } + + /// Attempt to create an `Ed25519PrivateKey` from an array of bytes. Not all byte buffers of the required size can create a valid `Ed25519PrivateKey`. Only buffers derived from [`Ed25519PrivateKey::to_bytes()`] are required to convert correctly. + /// + /// To securely generate a valid `Ed25519PrivateKey`, use [`Ed25519PrivateKey::generate()`]. + pub fn from_raw(raw: &[u8; ED25519_PRIVATE_KEY_SIZE]) -> Result { + Self::from_raw_impl(raw, FromRawValidationMethod::Ed25519Dalek) + } + + fn from_key_blob_impl( + key_blob: &str, + method: FromRawValidationMethod, + ) -> Result { + if key_blob.len() != ED25519_PRIVATE_KEY_KEYBLOB_LENGTH { + return Err(Error::ParseError(format!( + "expects string of length '{}'; received string with length '{}'", + ED25519_PRIVATE_KEY_KEYBLOB_LENGTH, + key_blob.len() + ))); + } + + if !key_blob.starts_with(ED25519_PRIVATE_KEY_KEYBLOB_HEADER) { + return Err(Error::ParseError(format!( + "expects string that begins with '{}'; received '{}'", + &ED25519_PRIVATE_KEY_KEYBLOB_HEADER, &key_blob + ))); + } + + let base64_key: &str = &key_blob[ED25519_PRIVATE_KEY_KEYBLOB_HEADER.len()..]; + let private_key_data = match BASE64.decode(base64_key.as_bytes()) { + Ok(private_key_data) => private_key_data, + Err(_) => { + return Err(Error::ParseError(format!( + "could not parse '{}' as base64", + base64_key + ))) + } + }; + let private_key_data_len = private_key_data.len(); + let private_key_data_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = match private_key_data.try_into() + { + Ok(private_key_data) => private_key_data, + Err(_) => { + return Err(Error::ParseError(format!( + "expects decoded private key length '{}'; actual '{}'", + ED25519_PRIVATE_KEY_SIZE, private_key_data_len + ))) + } + }; + + Ed25519PrivateKey::from_raw_impl(&private_key_data_raw, method) + } + + #[cfg(feature = "legacy-tor-provider")] + pub(crate) fn from_key_blob_legacy(key_blob: &str) -> Result { + Self::from_key_blob_impl(key_blob, FromRawValidationMethod::LegacyCTor) + } + + /// Create an `Ed25519PrivateKey` from a [`String`] in the legacy c-tor daemon key blob format used in the `ADD_ONION` control-port command. From the c-tor control [specification](https://spec.torproject.org/control-spec/commands.html#add_onion): + /// > For a "ED25519-V3" key is the Base64 encoding of the concatenation of the 32-byte ed25519 secret scalar in little-endian and the 32-byte ed25519 PRF secret. + /// + /// Only key blob strings derived from [`Ed25519PrivateKey::to_key_blob()`] are required to convert correctly. + pub fn from_key_blob(key_blob: &str) -> Result { + Self::from_key_blob_impl(key_blob, FromRawValidationMethod::Ed25519Dalek) + } + + /// Construct an `Ed25519PrivateKEy` from an [`X25519PrivateKey`]. + pub fn from_private_x25519( + x25519_private: &X25519PrivateKey, + ) -> Result<(Ed25519PrivateKey, SignBit), Error> { + if let Some((result, signbit)) = + convert_curve25519_to_ed25519_private(&x25519_private.secret_key) + { + Ok(( + Ed25519PrivateKey { + expanded_keypair: result, + }, + match signbit { + 0u8 => SignBit::Zero, + 1u8 => SignBit::One, + invalid_signbit => { + return Err(Error::ConversionError(format!( + "convert_curve25519_to_ed25519_private() returned invalid signbit: {}", + invalid_signbit + ))) + } + }, + )) + } else { + Err(Error::ConversionError( + "could not convert x25519 private key to ed25519 private key".to_string(), + )) + } + } + + /// Write `Ed25519PrivateKey` to a c-tor key blob formatted [`String`]. + pub fn to_key_blob(&self) -> String { + let mut key_blob = ED25519_PRIVATE_KEY_KEYBLOB_HEADER.to_string(); + key_blob.push_str(&BASE64.encode(&self.expanded_keypair.to_secret_key_bytes())); + + key_blob + } + + /// Sign the provided message and return an [`Ed25519Signature`]. + /// ## ⚠ Warning ⚠ + ///Only ever sign messages the private key owner controls the contents of! + pub fn sign_message(&self, message: &[u8]) -> Ed25519Signature { + let signature = self.expanded_keypair.sign(message); + Ed25519Signature { signature } + } + + /// Convert this private key to an array of bytes. + pub fn to_bytes(&self) -> [u8; ED25519_PRIVATE_KEY_SIZE] { + self.expanded_keypair.to_secret_key_bytes() + } + + #[cfg(feature = "arti-client-tor-provider")] + pub(crate) fn inner(&self) -> &pk::ed25519::ExpandedKeypair { + &self.expanded_keypair + } +} + +impl PartialEq for Ed25519PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.to_bytes().eq(&other.to_bytes()) + } +} + +impl Clone for Ed25519PrivateKey { + fn clone(&self) -> Ed25519PrivateKey { + match Ed25519PrivateKey::from_raw(&self.to_bytes()) { + Ok(ed25519_private_key) => ed25519_private_key, + Err(_) => unreachable!(), + } + } +} + +impl std::fmt::Debug for Ed25519PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "--- ed25519 private key ---") + } +} + +/// A wrapper around `tor_llcrypto::pk::ed25519::PublicKey` +impl Ed25519PublicKey { + /// Construct an `Ed25519PublicKey` from an array of bytes. Not all byte buffers of the required size can create a valid `Ed25519PublicKey`. Only buffers derived from [`Ed25519PublicKey::as_bytes()`] are required to convert correctly. + pub fn from_raw(raw: &[u8; ED25519_PUBLIC_KEY_SIZE]) -> Result { + Ok(Ed25519PublicKey { + public_key: match pk::ed25519::PublicKey::from_bytes(raw) { + Ok(public_key) => public_key, + Err(_) => return Err(Error::KeyInvalid), + }, + }) + } + + /// Construct an `Ed25519PublicKey` from a [`V3OnionServiceId`]. + pub fn from_service_id(service_id: &V3OnionServiceId) -> Result { + // decode base32 encoded service id + let mut decoded_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; + let decoded_byte_count = + match ONION_BASE32.decode_mut(service_id.as_bytes(), &mut decoded_service_id) { + Ok(decoded_byte_count) => decoded_byte_count, + Err(_) => { + return Err(Error::ConversionError(format!( + "failed to decode '{}' as V3OnionServiceId", + service_id + ))) + } + }; + if decoded_byte_count != V3_ONION_SERVICE_ID_RAW_SIZE { + return Err(Error::ConversionError(format!( + "decoded byte count is '{}', expected '{}'", + decoded_byte_count, V3_ONION_SERVICE_ID_RAW_SIZE + ))); + } + + Ed25519PublicKey::from_raw( + decoded_service_id[0..ED25519_PUBLIC_KEY_SIZE] + .try_into() + .unwrap(), + ) + } + + /// Construct an `Ed25519PublicKey` from an [`Ed25519PrivateKey`]. + pub fn from_private_key(private_key: &Ed25519PrivateKey) -> Ed25519PublicKey { + Ed25519PublicKey { + public_key: *private_key.expanded_keypair.public(), + } + } + + fn from_public_x25519( + public_x25519: &X25519PublicKey, + signbit: SignBit, + ) -> Result { + match convert_curve25519_to_ed25519_public(&public_x25519.public_key, signbit.into()) { + Some(public_key) => Ok(Ed25519PublicKey { public_key }), + None => Err(Error::ConversionError( + "failed to create ed25519 public key from x25519 public key and signbit" + .to_string(), + )), + } + } + + /// View this public key as an array of bytes + pub fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_SIZE] { + self.public_key.as_bytes() + } +} + +impl PartialEq for Ed25519PublicKey { + fn eq(&self, other: &Self) -> bool { + self.public_key.eq(&other.public_key) + } +} + +impl std::fmt::Debug for Ed25519PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.public_key.fmt(f) + } +} + + +/// A wrapper around `tor_llcrypto::pk::ed25519::Signature` +impl Ed25519Signature { + /// Construct an `Ed25519Signature` from an array of bytes. + pub fn from_raw(raw: &[u8; ED25519_SIGNATURE_SIZE]) -> Result { + // todo: message cannot fail so should not return a Result<> + Ok(Ed25519Signature { + signature: pk::ed25519::Signature::from_bytes(raw), + }) + } + + /// Verify this `Ed25519Signature` for the given message and [`Ed25519PublicKey`]. + pub fn verify(&self, message: &[u8], public_key: &Ed25519PublicKey) -> bool { + public_key + .public_key + .verify(message, &self.signature) + .is_ok() + } + + /// Verify this `Ed25519Signature` for the given message, [`X25519PublicKey`], and [`SignBit`]. This signature must have been created by first converting an [`X25519PrivateKey`] to a [`Ed25519PrivateKey`] and [`SignBit`], and then signing the message using this [`Ed25519PrivateKey`]. This method verifies the signature using the [`Ed25519PublicKey`] derived from the provided [`X25519PublicKey`] and [`SignBit`]. + pub fn verify_x25519( + &self, + message: &[u8], + public_key: &X25519PublicKey, + signbit: SignBit, + ) -> bool { + if let Ok(public_key) = Ed25519PublicKey::from_public_x25519(public_key, signbit) { + return self.verify(message, &public_key); + } + false + } + + /// Convert this signature to an array of bytes + pub fn to_bytes(&self) -> [u8; ED25519_SIGNATURE_SIZE] { + self.signature.to_bytes() + } +} + +impl PartialEq for Ed25519Signature { + fn eq(&self, other: &Self) -> bool { + self.signature.eq(&other.signature) + } +} + +impl std::fmt::Debug for Ed25519Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.signature.fmt(f) + } +} + +/// A wrapper around `tor_llcrypto::pk::curve25519::StaticSecret` +impl X25519PrivateKey { + /// Securely generate a new `X25519PrivateKey` + pub fn generate() -> X25519PrivateKey { + let csprng = &mut tor_llcrypto::rng::CautiousRng; + X25519PrivateKey { + secret_key: pk::curve25519::StaticSecret::random_from_rng(csprng), + } + } + + /// Attempt to create an `X25519PrivateKey` from an array of bytes. Not all byte buffers of the required size can create a valid `X25519PrivateKey`. Only buffers derived from [`X25519PrivateKey::to_bytes()`] are required to convert correctly. + /// + /// To securely generate a valid `X25519PrivateKey`, use [`X25519PrivateKey::generate()`]. + pub fn from_raw(raw: &[u8; X25519_PRIVATE_KEY_SIZE]) -> Result { + // see: https://docs.rs/x25519-dalek/2.0.0-pre.1/src/x25519_dalek/x25519.rs.html#197 + if raw[0] == raw[0] & 240 && raw[31] == (raw[31] & 127) | 64 { + Ok(X25519PrivateKey { + secret_key: pk::curve25519::StaticSecret::from(*raw), + }) + } else { + Err(Error::KeyInvalid) + } + } + + /// Create an `X25519PrivateKey` from a [`String`] in the legacy c-tor daemon key blob format used in the `ONION_CLIENT_AUTH_ADD` control-port command. From the c-tor control [specification](https://spec.torproject.org/control-spec/commands.html#onion_client_auth_add): + /// > ```text + /// > PrivateKeyBlob = base64 encoding of x25519 key + /// > ``` + /// + /// Only key blob strings derived from [`X25519PrivateKey::to_base64()`] are required to convert correctly. + pub fn from_base64(base64: &str) -> Result { + // todo: see if this should be from/to key blob like with ed25519 rather than base64 + if base64.len() != X25519_PRIVATE_KEY_BASE64_LENGTH { + return Err(Error::ParseError(format!( + "expects string of length '{}'; received string with length '{}'", + X25519_PRIVATE_KEY_BASE64_LENGTH, + base64.len() + ))); + } + + let private_key_data = match BASE64.decode(base64.as_bytes()) { + Ok(private_key_data) => private_key_data, + Err(_) => { + return Err(Error::ParseError(format!( + "could not parse '{}' as base64", + base64 + ))) + } + }; + let private_key_data_len = private_key_data.len(); + let private_key_data_raw: [u8; X25519_PRIVATE_KEY_SIZE] = match private_key_data.try_into() + { + Ok(private_key_data) => private_key_data, + Err(_) => { + return Err(Error::ParseError(format!( + "expects decoded private key length '{}'; actual '{}'", + X25519_PRIVATE_KEY_SIZE, private_key_data_len + ))) + } + }; + + X25519PrivateKey::from_raw(&private_key_data_raw) + } + + /// Sign the provided message and return an [`Ed25519Signature`] and [`SignBit`]. + /// + /// This method first converts this `X25519PrivateKey` to an [`Ed25519PrivateKey`] and [`SignBit`]. Then, the message is signed using the derived [`Ed25519PrivateKey`]. To verify the signature, both the [`X25519PublicKey`] and this calculated [`SignBit`] are required. + /// + /// ## ⚠ Warning ⚠ + ///Only ever sign messages the private key owner controls the contents of! + pub fn sign_message(&self, message: &[u8]) -> Result<(Ed25519Signature, SignBit), Error> { + let (ed25519_private, signbit) = Ed25519PrivateKey::from_private_x25519(self)?; + Ok((ed25519_private.sign_message(message), signbit)) + } + + /// Write `X25519PrivateKey` to a base64 encocded [`String`]. + pub fn to_base64(&self) -> String { + BASE64.encode(&self.secret_key.to_bytes()) + } + + /// Convert this private key to an array of bytes. + pub fn to_bytes(&self) -> [u8; X25519_PRIVATE_KEY_SIZE] { + self.secret_key.to_bytes() + } + + #[cfg(feature = "arti-client-tor-provider")] + pub(crate) fn inner(&self) -> &pk::curve25519::StaticSecret { + &self.secret_key + } +} + +impl PartialEq for X25519PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.secret_key.to_bytes() == other.secret_key.to_bytes() + } +} + +impl std::fmt::Debug for X25519PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "--- x25519 private key ---") + } +} + +/// A wrapper around `tor_llcrypto::pk::curve25519::PublicKey` +impl X25519PublicKey { + /// Construct an `X25519PublicKey` from an [`X25519PrivateKey`]. + pub fn from_private_key(private_key: &X25519PrivateKey) -> X25519PublicKey { + X25519PublicKey { + public_key: pk::curve25519::PublicKey::from(&private_key.secret_key), + } + } + + /// Construct an `X25519PublicKey` from an array of bytes. + pub fn from_raw(raw: &[u8; X25519_PUBLIC_KEY_SIZE]) -> X25519PublicKey { + X25519PublicKey { + public_key: pk::curve25519::PublicKey::from(*raw), + } + } + + /// Create an `X25519PublicKey` from a [`String`] in the legacy c-tor daemon key base32 format used in the `ADD_ONION` control-port command. From the c-tor control [specification](https://spec.torproject.org/control-spec/commands.html#add_onion): + /// > ```text + /// > V3Key = The client's base32-encoded x25519 public key, using only the key + /// > part of rend-spec-v3.txt section G.1.2 (v3 only). + /// > ``` + /// + /// Only key base32 strings derived from [`X25519PublicKey::to_base32()`] are required to convert correctly. + pub fn from_base32(base32: &str) -> Result { + if base32.len() != X25519_PUBLIC_KEY_BASE32_LENGTH { + return Err(Error::ParseError(format!( + "expects string of length '{}'; received '{}' with length '{}'", + X25519_PUBLIC_KEY_BASE32_LENGTH, + base32, + base32.len() + ))); + } + + let public_key_data = match BASE32_NOPAD.decode(base32.as_bytes()) { + Ok(public_key_data) => public_key_data, + Err(_) => { + return Err(Error::ParseError(format!( + "failed to decode '{}' as X25519PublicKey", + base32 + ))) + } + }; + let public_key_data_len = public_key_data.len(); + let public_key_data_raw: [u8; X25519_PUBLIC_KEY_SIZE] = match public_key_data.try_into() { + Ok(public_key_data) => public_key_data, + Err(_) => { + return Err(Error::ParseError(format!( + "expects decoded public key length '{}'; actual '{}'", + X25519_PUBLIC_KEY_SIZE, public_key_data_len + ))) + } + }; + + Ok(X25519PublicKey::from_raw(&public_key_data_raw)) + } + + /// Write `X25519PublicKey` to a base32 encocded [`String`]. + pub fn to_base32(&self) -> String { + BASE32_NOPAD.encode(self.public_key.as_bytes()) + } + + /// View this public key as an array of bytes + pub fn as_bytes(&self) -> &[u8; X25519_PUBLIC_KEY_SIZE] { + self.public_key.as_bytes() + } + + #[cfg(feature = "arti-client-tor-provider")] + pub(crate) fn inner(&self) -> &pk::curve25519::PublicKey { + &self.public_key + } + +} + +impl std::fmt::Debug for X25519PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_base32()) + } +} + +/// Strongly-typed representation of a v3 onion-service id +impl V3OnionServiceId { + // see https://github.com/torproject/torspec/blob/main/rend-spec-v3.txt#L2143 + fn calc_truncated_checksum( + public_key: &[u8; ED25519_PUBLIC_KEY_SIZE], + ) -> [u8; TRUNCATED_CHECKSUM_SIZE] { + let mut hasher = Sha3_256::new(); + + // calculate checksum + hasher.update(b".onion checksum"); + hasher.update(public_key); + hasher.update([0x03u8]); + let hash_bytes = hasher.finalize(); + + [hash_bytes[0], hash_bytes[1]] + } + + /// Create a `V3OnionServiceId` from a [`String`] in the version 3 onion service digest format. From the tor address [specification](https://spec.torproject.org/address-spec.html#onion): + /// > ```text + /// > onion_address = base32(PUBKEY | CHECKSUM | VERSION) + /// > CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2] + /// > + /// > where: + /// > - PUBKEY is the 32 bytes ed25519 master pubkey of the onion service. + /// > - VERSION is a one byte version field (default value '\x03') + /// > - ".onion checksum" is a constant string + /// > - H is SHA3-256 + /// > - CHECKSUM is truncated to two bytes before inserting it in onion_address + /// > ``` + pub fn from_string(service_id: &str) -> Result { + if !V3OnionServiceId::is_valid(service_id) { + return Err(Error::ParseError(format!( + "'{}' is not a valid v3 onion service id", + service_id + ))); + } + Ok(V3OnionServiceId { + data: service_id.as_bytes().try_into().unwrap(), + }) + } + + /// Create a `V3OnionServiceId` from an [`Ed25519PublicKey`]. + pub fn from_public_key(public_key: &Ed25519PublicKey) -> V3OnionServiceId { + let mut raw_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; + + raw_service_id[..ED25519_PUBLIC_KEY_SIZE].copy_from_slice(&public_key.as_bytes()[..]); + let truncated_checksum = Self::calc_truncated_checksum(public_key.as_bytes()); + raw_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET] = truncated_checksum[0]; + raw_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET + 1] = truncated_checksum[1]; + raw_service_id[V3_ONION_SERVICE_ID_VERSION_OFFSET] = 0x03u8; + + let mut service_id = [0u8; V3_ONION_SERVICE_ID_STRING_LENGTH]; + // panics on wrong buffer size, but given our constant buffer sizes should be fine + ONION_BASE32.encode_mut(&raw_service_id, &mut service_id); + + V3OnionServiceId { data: service_id } + } + + /// Create a `V3OnionServiceId` from an [`Ed25519PrivateKey`]. + pub fn from_private_key(private_key: &Ed25519PrivateKey) -> V3OnionServiceId { + Self::from_public_key(&Ed25519PublicKey::from_private_key(private_key)) + } + + /// Determine if the provided string is a valid representation of a `V3OnionServiceId` + pub fn is_valid(service_id: &str) -> bool { + if service_id.len() != V3_ONION_SERVICE_ID_STRING_LENGTH { + return false; + } + + let mut decoded_service_id = [0u8; V3_ONION_SERVICE_ID_RAW_SIZE]; + match ONION_BASE32.decode_mut(service_id.as_bytes(), &mut decoded_service_id) { + Ok(decoded_byte_count) => { + // ensure right size + if decoded_byte_count != V3_ONION_SERVICE_ID_RAW_SIZE { + return false; + } + // ensure correct version + if decoded_service_id[V3_ONION_SERVICE_ID_VERSION_OFFSET] != 0x03 { + return false; + } + // copy public key into own buffer + let mut public_key = [0u8; ED25519_PUBLIC_KEY_SIZE]; + public_key[..].copy_from_slice(&decoded_service_id[..ED25519_PUBLIC_KEY_SIZE]); + // ensure checksum is correct + let truncated_checksum = Self::calc_truncated_checksum(&public_key); + if truncated_checksum[0] != decoded_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET] + || truncated_checksum[1] + != decoded_service_id[V3_ONION_SERVICE_ID_CHECKSUM_OFFSET + 1] + { + return false; + } + true + } + Err(_) => false, + } + } + + /// View this service id as an array of bytes + pub fn as_bytes(&self) -> &[u8; V3_ONION_SERVICE_ID_STRING_LENGTH] { + &self.data + } +} + +impl std::fmt::Display for V3OnionServiceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unsafe { write!(f, "{}", str::from_utf8_unchecked(&self.data)) } + } +} + +impl std::fmt::Debug for V3OnionServiceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unsafe { write!(f, "{}", str::from_utf8_unchecked(&self.data)) } + } +} diff --git a/tor-interface/src/tor_provider.rs b/tor-interface/src/tor_provider.rs new file mode 100644 index 000000000..53e04b40e --- /dev/null +++ b/tor-interface/src/tor_provider.rs @@ -0,0 +1,750 @@ +// standard +use std::any::Any; +use std::boxed::Box; +use std::convert::TryFrom; +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener}; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; +use std::sync::{atomic, Arc, OnceLock}; +#[cfg(unix)] +use std::os::unix::io::{IntoRawFd, RawFd}; +#[cfg(windows)] +use std::os::windows::io::{IntoRawSocket, RawSocket}; + +// extern crates +use domain::base::name::Name; +use idna::uts46::{Hyphens, Uts46}; +use idna::{domain_to_ascii_cow, AsciiDenyList}; +use regex::Regex; +pub use socks::TcpOrUnixStream; + +// internal crates +use crate::tor_crypto::*; + + +/// Various `tor_provider` errors. +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to parse '{0}' as {1}")] + /// Failure parsing some string into a type + ParseFailure(String, String), + + #[error("{0}")] + /// Other miscellaneous error + Generic(String), +} + +// +// OnionAddr +// + +/// A version 3 onion service address. +/// +/// Version 3 Onion Service addresses const of a [`crate::tor_crypto::V3OnionServiceId`] and a 16-bit port number. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OnionAddrV3 { + pub(crate) service_id: V3OnionServiceId, + pub(crate) virt_port: u16, +} + +impl OnionAddrV3 { + /// Create a new `OnionAddrV3` from a [`crate::tor_crypto::V3OnionServiceId`] and port number. + pub fn new(service_id: V3OnionServiceId, virt_port: u16) -> OnionAddrV3 { + OnionAddrV3 { + service_id, + virt_port, + } + } + + /// Return the service id associated with this onion address. + pub fn service_id(&self) -> &V3OnionServiceId { + &self.service_id + } + + /// Return the port numebr associated with this onion address. + pub fn virt_port(&self) -> u16 { + self.virt_port + } +} + +impl std::fmt::Display for OnionAddrV3 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.onion:{}", self.service_id, self.virt_port) + } +} + +/// An onion service address analog to [`std::net::SocketAddr`] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum OnionAddr { + V3(OnionAddrV3), +} + +impl FromStr for OnionAddr { + type Err = Error; + fn from_str(s: &str) -> Result { + static ONION_SERVICE_PATTERN: OnceLock = OnceLock::new(); + let onion_service_pattern = ONION_SERVICE_PATTERN.get_or_init(|| { + Regex::new(r"(?m)^(?P[a-z2-7]{56})\.onion:(?P[1-9][0-9]{0,4})$") + .unwrap() + }); + + if let Some(caps) = onion_service_pattern.captures(s.to_lowercase().as_ref()) { + let service_id = caps + .name("service_id") + .expect("missing service_id group") + .as_str() + .to_lowercase(); + let port = caps.name("port").expect("missing port group").as_str(); + if let (Ok(service_id), Ok(port)) = ( + V3OnionServiceId::from_string(service_id.as_ref()), + u16::from_str(port), + ) { + return Ok(OnionAddr::V3(OnionAddrV3::new(service_id, port))); + } + } + Err(Self::Err::ParseFailure(s.to_string(), "OnionAddr".to_string())) + } +} + +impl std::fmt::Display for OnionAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OnionAddr::V3(onion_addr) => onion_addr.fmt(f), + } + } +} + +// +// DomainAddr +// + +/// A domain name analog to `std::net::SocketAddr` +/// +/// A `DomainAddr` must not end in ".onion" +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DomainAddr { + domain: String, + port: u16, +} + +/// A `DomainAddr` has a domain name (scuh as `www.example.com`) and a port +impl DomainAddr { + /// Returns the domain name associated with this domain address. + pub fn domain(&self) -> &str { + self.domain.as_ref() + } + + /// Returns the port number associated with this domain address. + pub fn port(&self) -> u16 { + self.port + } +} + +impl std::fmt::Display for DomainAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let uts46: Uts46 = Default::default(); + let (ui_str, _err) = uts46.to_user_interface( + self.domain.as_str().as_bytes(), + AsciiDenyList::URL, + Hyphens::Allow, + |_, _, _| -> bool { false }, + ); + write!(f, "{}:{}", ui_str, self.port) + } +} + +impl TryFrom<(String, u16)> for DomainAddr { + type Error = Error; + + fn try_from(value: (String, u16)) -> Result { + let (domain, port) = (&value.0, value.1); + if let Ok(domain) = domain_to_ascii_cow(domain.as_bytes(), AsciiDenyList::URL) { + let domain = domain.to_string(); + if let Ok(domain) = Name::>::from_str(domain.as_ref()) { + let domain = domain.to_string(); + if !domain.ends_with(".onion") { + return Ok(Self { + domain, + port, + }); + } + } + } + Err(Self::Error::ParseFailure(format!( + "{}:{}", + domain, port + ), "DomainAddr".to_string())) + } +} + +impl FromStr for DomainAddr { + type Err = Error; + fn from_str(s: &str) -> Result { + static DOMAIN_PATTERN: OnceLock = OnceLock::new(); + let domain_pattern = DOMAIN_PATTERN + .get_or_init(|| Regex::new(r"(?m)^(?P.*):(?P[1-9][0-9]{0,4})$").unwrap()); + if let Some(caps) = domain_pattern.captures(s) { + let domain = caps + .name("domain") + .expect("missing domain group") + .as_str() + .to_string(); + let port = caps.name("port").expect("missing port group").as_str(); + if let Ok(port) = u16::from_str(port) { + return Self::try_from((domain, port)); + } + } + Err(Self::Err::ParseFailure(s.to_string(), "DomainAddr".to_string())) + } +} + +// +// TargetAddr +// + +/// An enum representing the various types of addresses a [`TorProvider`] implementation may connect to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TargetAddr { + /// An ip address and port + Socket(std::net::SocketAddr), + /// An onion-service id and virtual port + OnionService(OnionAddr), + /// A domain name and port + Domain(DomainAddr), +} + +impl From<(V3OnionServiceId, u16)> for TargetAddr { + fn from(target_tuple: (V3OnionServiceId, u16)) -> Self { + TargetAddr::OnionService(OnionAddr::V3(OnionAddrV3::new( + target_tuple.0, + target_tuple.1, + ))) + } +} + +impl FromStr for TargetAddr { + type Err = Error; + fn from_str(s: &str) -> Result { + if let Ok(socket_addr) = SocketAddr::from_str(s) { + return Ok(TargetAddr::Socket(socket_addr)); + } else if let Ok(onion_addr) = OnionAddr::from_str(s) { + return Ok(TargetAddr::OnionService(onion_addr)); + } else if let Ok(domain_addr) = DomainAddr::from_str(s) { + return Ok(TargetAddr::Domain(domain_addr)); + } + Err(Self::Err::ParseFailure(s.to_string(), "TargetAddr".to_string())) + } +} + +impl std::fmt::Display for TargetAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TargetAddr::Socket(socket_addr) => socket_addr.fmt(f), + TargetAddr::OnionService(onion_addr) => onion_addr.fmt(f), + TargetAddr::Domain(domain_addr) => domain_addr.fmt(f), + } + } +} + +/// Various events possibly returned by a [`TorProvider`] implementation's `update()` method. +#[derive(Debug)] +pub enum TorEvent { + /// A status update received connecting to the Tor Network. + BootstrapStatus { + /// A number from 0 to 100 for how through the bootstrap process the `TorProvider` is. + progress: u32, + /// A short string to identify the current phase of the bootstrap process. + tag: String, + /// A longer string with a summary of the current phase of the bootstrap process. + summary: String, + }, + /// Indicates successful connection to the Tor Network. The [`TorProvider::connect()`] and [`TorProvider::listener()`] methods may now be used. + BootstrapComplete, + /// Messages which may be useful for troubleshooting. + LogReceived { + /// A message + line: String, + }, + /// An onion-service has been published to the Tor Network and may now be reachable by clients. + OnionServicePublished { + /// The service-id of the onion-service which has been published. + service_id: V3OnionServiceId, + }, +} + +/// A `CircuitToken` is used to specify circuits used to connect to clearnet services. +pub type CircuitToken = usize; + +// +// Onion Stream +// + +#[cfg(unix)] +pub type OnionStreamIntoRaw = RawFd; +#[cfg(windows)] +pub type OnionStreamIntoRaw = RawSocket; + +/// A wrapper around a [`TcpOrUnixStream`] with some Tor-specific customisations +/// +/// An onion-listener can be constructed using the [`TorProvider::connect()`] method. +pub trait OnionStream: Send + Read + Write + std::fmt::Debug { + /// Returns the target address of the remote peer of this onion connection. + fn peer_addr(&self) -> Option; + + /// Returns the onion address of the local connection for an incoming onion-service connection. Returns `None` for outgoing connections. + fn local_addr(&self) -> Option; + + /// Tries to clone the underlying connection and data. A simple pass-through to [`TcpOrUnixStream::try_clone()`]. + fn try_clone(&self) -> std::io::Result where Self: Sized; + + /// Moves the underlying `TcpOrUnixStream` into or out of nonblocking mode. + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()>; + + /// Consume stream and return the underlying raw handle. + fn into_raw(self) -> OnionStreamIntoRaw; +} + +#[derive(Debug)] +pub struct TcpOrUnixOnionStream { + pub(crate) stream: TcpOrUnixStream, + pub(crate) local_addr: Option, + pub(crate) peer_addr: Option, +} + +impl Deref for TcpOrUnixOnionStream { + type Target = TcpOrUnixStream; + fn deref(&self) -> &Self::Target { + &self.stream + } +} + +impl DerefMut for TcpOrUnixOnionStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.stream + } +} + +impl From for TcpOrUnixStream { + fn from(onion_stream: TcpOrUnixOnionStream) -> Self { + onion_stream.stream + } +} + +impl Read for TcpOrUnixOnionStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.stream.read(buf) + } + fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { + self.stream.read_vectored(bufs) + } + fn read_to_end(&mut self, buf: &mut Vec) -> std::io::Result { + self.stream.read_to_end(buf) + } + fn read_to_string(&mut self, buf: &mut String) -> std::io::Result { + self.stream.read_to_string(buf) + } + fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + self.stream.read_exact(buf) + } +} + +impl Write for TcpOrUnixOnionStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.stream.write(buf) + } + fn flush(&mut self) -> std::io::Result<()> { + self.stream.flush() + } + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + self.stream.write_vectored(bufs) + } + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + self.stream.write_all(buf) + } + fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> { + self.stream.write_fmt(fmt) + } +} + +impl OnionStream for TcpOrUnixOnionStream { + fn peer_addr(&self) -> Option { + self.peer_addr.clone() + } + + fn local_addr(&self) -> Option { + self.local_addr.clone() + } + + fn try_clone(&self) -> std::io::Result where Self: Sized { + Ok(Self { + stream: self.stream.try_clone()?, + local_addr: self.local_addr.clone(), + peer_addr: self.peer_addr.clone(), + }) + } + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.stream.set_nonblocking(nonblocking) + } + + fn into_raw(self) -> OnionStreamIntoRaw { + #[cfg(unix)] + return self.stream.into_raw_fd(); + #[cfg(windows)] + return self.stream.into_raw_stream(); + } +} + +pub struct BoxOnionStream { + data: Box, + + peer_addr: fn(&Box) -> Option, + local_addr: fn(&Box) -> Option, + try_clone: fn(&Box) -> std::io::Result, + set_nonblocking: fn(&Box, bool) -> std::io::Result<()>, + into_raw: fn(Box) -> OnionStreamIntoRaw, + + read: fn(&mut Box, &mut [u8]) -> std::io::Result, + read_vectored: fn(&mut Box, &mut [std::io::IoSliceMut<'_>]) -> std::io::Result, + read_to_end: fn(&mut Box, &mut Vec) -> std::io::Result, + read_to_string: fn(&mut Box, &mut String) -> std::io::Result, + read_exact: fn(&mut Box, &mut [u8]) -> std::io::Result<()>, + + write: fn(&mut Box, &[u8]) -> std::io::Result, + flush: fn(&mut Box) -> std::io::Result<()>, + write_vectored: fn(&mut Box, &[std::io::IoSlice<'_>]) -> std::io::Result, + write_all: fn(&mut Box, &[u8]) -> std::io::Result<()>, + write_fmt: fn(&mut Box, std::fmt::Arguments<'_>) -> std::io::Result<()>, +} + +impl std::fmt::Debug for BoxOnionStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("BoxOnionStream") + } +} + +impl BoxOnionStream { + pub fn new(s: S) -> Self { + Self { + data: Box::new(s), + + peer_addr: |slf| slf.downcast_ref::().unwrap().peer_addr(), + local_addr: |slf| slf.downcast_ref::().unwrap().local_addr(), + try_clone: |slf| slf.downcast_ref::().unwrap().try_clone().map(BoxOnionStream::new), + set_nonblocking: |slf, nonblocking| slf.downcast_ref::().unwrap().set_nonblocking(nonblocking), + into_raw: |slf| slf.downcast::().unwrap().into_raw(), + + read: |slf, buf| slf.downcast_mut::().unwrap().read(buf), + read_vectored: |slf, bufs| slf.downcast_mut::().unwrap().read_vectored(bufs), + read_to_end: |slf, buf| slf.downcast_mut::().unwrap().read_to_end(buf), + read_to_string: |slf, buf| slf.downcast_mut::().unwrap().read_to_string(buf), + read_exact: |slf, buf| slf.downcast_mut::().unwrap().read_exact(buf), + + write: |slf, buf| slf.downcast_mut::().unwrap().write(buf), + flush: |slf| slf.downcast_mut::().unwrap().flush(), + write_vectored: |slf, bufs| slf.downcast_mut::().unwrap().write_vectored(bufs), + write_all: |slf, buf| slf.downcast_mut::().unwrap().write_all(buf), + write_fmt: |slf, fmt| slf.downcast_mut::().unwrap().write_fmt(fmt), + } + } +} + +impl Read for BoxOnionStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + (self.read)(&mut self.data, buf) + } + fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result { + (self.read_vectored)(&mut self.data, bufs) + } + fn read_to_end(&mut self, buf: &mut Vec) -> std::io::Result { + (self.read_to_end)(&mut self.data, buf) + } + fn read_to_string(&mut self, buf: &mut String) -> std::io::Result { + (self.read_to_string)(&mut self.data, buf) + } + fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> { + (self.read_exact)(&mut self.data, buf) + } +} + +impl Write for BoxOnionStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + (self.write)(&mut self.data, buf) + } + fn flush(&mut self) -> std::io::Result<()> { + (self.flush)(&mut self.data) + } + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + (self.write_vectored)(&mut self.data, bufs) + } + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + (self.write_all)(&mut self.data, buf) + } + fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> { + (self.write_fmt)(&mut self.data, fmt) + } +} + +impl OnionStream for BoxOnionStream { + fn peer_addr(&self) -> Option { + (self.peer_addr)(&self.data) + } + + fn local_addr(&self) -> Option { + (self.local_addr)(&self.data) + } + + fn try_clone(&self) -> std::io::Result where Self: Sized { + (self.try_clone)(&self.data) + } + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + (self.set_nonblocking)(&self.data, nonblocking) + } + + fn into_raw(self) -> OnionStreamIntoRaw { + (self.into_raw)(self.data) + } +} + +// +// Onion Listener +// + +/// A wrapper around a [`std::net::TcpListener`] with some Tor-specific customisations. +/// +/// An onion-listener can be constructed using the [`TorProvider::listener()`] method. +pub trait OnionListener: Send { + type Stream: OnionStream; + + /// Moves the underlying `TcpListener` into or out of nonblocking mode. + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()>; + + /// Accept a new incoming connection from this listener. + fn accept(&self) -> std::io::Result>; + + /// Address this listener is listening on + fn address(&self) -> &OnionAddr; +} + +pub(crate) struct TcpOnionListenerBase(pub TcpListener, pub OnionAddr); + +pub struct TcpOnionListener(pub(crate) TcpOnionListenerBase, pub(crate) Arc); + +impl OnionListener for TcpOnionListenerBase { + type Stream = TcpOrUnixOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.0.set_nonblocking(nonblocking) + } + + fn accept(&self) -> std::io::Result> { + match self.0.accept() { + Ok((stream, _socket_addr)) => Ok(Some(TcpOrUnixOnionStream { + stream: stream.into(), + local_addr: Some(self.1.clone()), + peer_addr: None, + })), + Err(err) => { + if err.kind() == std::io::ErrorKind::WouldBlock { + Ok(None) + } else { + Err(err) + } + } + } + } + + fn address(&self) -> &OnionAddr { + &self.1 + } +} + +impl OnionListener for TcpOnionListener { + type Stream = TcpOrUnixOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + self.0.set_nonblocking(nonblocking) + } + + fn accept(&self) -> std::io::Result> { + self.0.accept() + } + + fn address(&self) -> &OnionAddr { + &self.0.address() + } +} + +impl TcpOnionListener { + /// `TcpListener::try_clone()` the inner listener + /// + /// The lifetime of the hidden service itself is still bound to this object, + /// but the resulting [`TcpListener`] may be polled/`accept`ed independently + pub fn try_clone_inner(&self) -> std::io::Result { + self.0.0.try_clone() + } +} + +impl Drop for TcpOnionListener { + fn drop(&mut self) { + self.1.store(false, atomic::Ordering::Relaxed) + } +} + +pub struct BoxOnionListener { + data: Box, + + set_nonblocking: fn(&Box, bool) -> std::io::Result<()>, + accept: fn(&Box) -> std::io::Result::Stream>>, + address: fn(&Box) -> &OnionAddr, +} + +impl BoxOnionListener { + pub fn new(l: L) -> Self { + Self { + data: Box::new(l), + + set_nonblocking: |slf, nonblocking| slf.downcast_ref::().unwrap().set_nonblocking(nonblocking), + accept: |slf| slf.downcast_ref::().unwrap().accept().map(|r| r.map(BoxOnionStream::new)), + address: |slf| slf.downcast_ref::().unwrap().address(), + } + } +} + +impl OnionListener for BoxOnionListener { + type Stream = BoxOnionStream; + + fn set_nonblocking(&self, nonblocking: bool) -> std::io::Result<()> { + (self.set_nonblocking)(&self.data, nonblocking) + } + + fn accept(&self) -> std::io::Result> { + (self.accept)(&self.data) + } + + fn address(&self) -> &OnionAddr { + (self.address)(&self.data) + } +} + +/// The `TorProvider` trait allows for high-level Tor Network functionality. Implementations ay connect to the Tor Network, anonymously connect to both clearnet and onion-service endpoints, and host onion-services. +pub trait TorProvider: Send { + type Stream: OnionStream; + type Listener: OnionListener; + + /// Process and return `TorEvent`s handled by this `TorProvider`. + fn update(&mut self) -> Result, Error>; + /// Begin connecting to the Tor Network. + fn bootstrap(&mut self) -> Result<(), Error>; + /// Add v3 onion-service authorisation credentials, allowing this `TorProvider` to connect to an onion-service whose service-descriptor is encrypted using the assocciated x25519 public key. + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), Error>; + /// Remove a previously added client authorisation credential. This `TorProvider` will be unable to connect to the onion-service associated with the removed credentail. + fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error>; + /// Anonymously connect to the address specified by `target` over the Tor Network and return the associated [`OnionStream`]. + /// + /// When conecting to clearnet targets, an optional [`CircuitToken`] may be used to enforce usage of different circuits through the Tor Network. If `circuit` is `None`, the default circuit is used. + /// + ///Connections made with different `CircuitToken`s are required to use different circuits through the Tor Network. However, connections made with identical `CircuitToken`s are *not* required to use identical circuits through the Tor Network. + /// + /// Specifying a circuit token when connecting to an onion-service has no effect on the resulting circuit. + fn connect( + &mut self, + target: TargetAddr, + circuit: Option, + ) -> Result; + /// Anonymously start an onion-service and return the associated [`OnionListener`]. + /// + ///The resulting onion-service will not be reachable by clients until [`TorProvider::update()`] returns a [`TorEvent::OnionServicePublished`] event. The optional `authorised_clients` parameter may be used to require client authorisation keys to connect to resulting onion-service. `bind_addr` may be used to force a specific address and port. For further information, see the Tor Project's onion-services [client-auth documentation](https://community.torproject.org/onion-services/advanced/client-auth). + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorised_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, + ) -> Result; + /// Create a new [`CircuitToken`]. + fn generate_token(&mut self) -> CircuitToken; + /// Releaes a previously generated [`CircuitToken`]. + fn release_token(&mut self, token: CircuitToken); +} + + +pub struct BoxTorProvider { + data: Box, + + update: fn(&mut Box) -> Result, Error>, + bootstrap: fn(&mut Box) -> Result<(), Error>, + add_client_auth: fn(&mut Box, &V3OnionServiceId, &X25519PrivateKey) -> Result<(), Error>, + remove_client_auth: fn(&mut Box, &V3OnionServiceId) -> Result<(), Error>, + connect: fn(&mut Box, TargetAddr, Option) -> Result<::Stream, Error>, + listener: fn(&mut Box, &Ed25519PrivateKey, u16, Option<&[X25519PublicKey]>, Option) -> Result<::Listener, Error>, + generate_token: fn(&mut Box) -> CircuitToken, + release_token: fn(&mut Box, CircuitToken), +} + +impl BoxTorProvider { + pub fn new(p: P) -> Self { + Self { + data: Box::new(p), + + update: |slf| slf.downcast_mut::

().unwrap().update(), + bootstrap: |slf| slf.downcast_mut::

().unwrap().bootstrap(), + add_client_auth: |slf, service_id, client_auth| slf.downcast_mut::

().unwrap().add_client_auth(service_id, client_auth), + remove_client_auth: |slf, service_id| slf.downcast_mut::

().unwrap().remove_client_auth(service_id), + connect: |slf, target, circuit| slf.downcast_mut::

().unwrap().connect(target, circuit).map(BoxOnionStream::new), + listener: |slf, private_key, virt_port, authorised_clients, bind_addr| slf.downcast_mut::

().unwrap().listener(private_key, virt_port, authorised_clients, bind_addr).map(BoxOnionListener::new), + generate_token: |slf| slf.downcast_mut::

().unwrap().generate_token(), + release_token: |slf, token| slf.downcast_mut::

().unwrap().release_token(token), + } + } +} + +impl TorProvider for BoxTorProvider { + type Stream = BoxOnionStream; + type Listener = BoxOnionListener; + + fn update(&mut self) -> Result, Error> { + (self.update)(&mut self.data) + } + fn bootstrap(&mut self) -> Result<(), Error> { + (self.bootstrap)(&mut self.data) + } + fn add_client_auth( + &mut self, + service_id: &V3OnionServiceId, + client_auth: &X25519PrivateKey, + ) -> Result<(), Error> { + (self.add_client_auth)(&mut self.data, service_id, client_auth) + } + fn remove_client_auth(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> { + (self.remove_client_auth)(&mut self.data, service_id) + } + fn connect( + &mut self, + target: TargetAddr, + circuit: Option, + ) -> Result { + (self.connect)(&mut self.data, target, circuit) + } + fn listener( + &mut self, + private_key: &Ed25519PrivateKey, + virt_port: u16, + authorised_clients: Option<&[X25519PublicKey]>, + bind_addr: Option, + ) -> Result { + (self.listener)(&mut self.data, private_key, virt_port, authorised_clients, bind_addr) + } + fn generate_token(&mut self) -> CircuitToken { + (self.generate_token)(&mut self.data) + } + fn release_token(&mut self, token: CircuitToken) { + (self.release_token)(&mut self.data, token) + } +} diff --git a/tor-interface/tests/tor_crypto.rs b/tor-interface/tests/tor_crypto.rs new file mode 100644 index 000000000..5fda4e23b --- /dev/null +++ b/tor-interface/tests/tor_crypto.rs @@ -0,0 +1,139 @@ +// internal crates +use tor_interface::tor_crypto::*; + +#[test] +fn test_crypto_ed25519() -> Result<(), anyhow::Error> { + let private_key_blob = "ED25519-V3:rP3u8mZaKohap0lKsB8Z8qXbXqK456JKKGONDBhV+gPBVKa2mHVQqnRTVuFXe3inU3YW6qvc7glYEwe9rK0LhQ=="; + let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ + 0xacu8, 0xfdu8, 0xeeu8, 0xf2u8, 0x66u8, 0x5au8, 0x2au8, 0x88u8, 0x5au8, 0xa7u8, 0x49u8, + 0x4au8, 0xb0u8, 0x1fu8, 0x19u8, 0xf2u8, 0xa5u8, 0xdbu8, 0x5eu8, 0xa2u8, 0xb8u8, 0xe7u8, + 0xa2u8, 0x4au8, 0x28u8, 0x63u8, 0x8du8, 0x0cu8, 0x18u8, 0x55u8, 0xfau8, 0x03u8, 0xc1u8, + 0x54u8, 0xa6u8, 0xb6u8, 0x98u8, 0x75u8, 0x50u8, 0xaau8, 0x74u8, 0x53u8, 0x56u8, 0xe1u8, + 0x57u8, 0x7bu8, 0x78u8, 0xa7u8, 0x53u8, 0x76u8, 0x16u8, 0xeau8, 0xabu8, 0xdcu8, 0xeeu8, + 0x09u8, 0x58u8, 0x13u8, 0x07u8, 0xbdu8, 0xacu8, 0xadu8, 0x0bu8, 0x85u8, + ]; + let public_raw: [u8; ED25519_PUBLIC_KEY_SIZE] = [ + 0xf2u8, 0xfdu8, 0xa2u8, 0xdbu8, 0xf3u8, 0x80u8, 0xa6u8, 0xbau8, 0x74u8, 0xa4u8, 0x90u8, + 0xe1u8, 0x45u8, 0x55u8, 0xeeu8, 0xb9u8, 0x32u8, 0xa0u8, 0x5cu8, 0x39u8, 0x5au8, 0xe2u8, + 0x02u8, 0x83u8, 0x55u8, 0x27u8, 0x89u8, 0x6au8, 0x1fu8, 0x2fu8, 0x3du8, 0xc5u8, + ]; + let service_id_string = "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"; + assert!(V3OnionServiceId::is_valid(&service_id_string)); + + let mut message = [0x00u8; 256]; + let null_message = [0x00u8; 256]; + for (i, ptr) in message.iter_mut().enumerate() { + *ptr = i as u8; + } + let signature_raw: [u8; ED25519_SIGNATURE_SIZE] = [ + 0xa6u8, 0xd6u8, 0xc6u8, 0x1au8, 0x03u8, 0xbcu8, 0x43u8, 0x6fu8, 0x38u8, 0x53u8, 0x94u8, + 0xcdu8, 0xdcu8, 0x86u8, 0x0au8, 0x88u8, 0x64u8, 0x43u8, 0x1du8, 0x18u8, 0x84u8, 0x30u8, + 0x2fu8, 0xcdu8, 0xa6u8, 0x79u8, 0xcau8, 0x87u8, 0xd0u8, 0x29u8, 0xe7u8, 0x2bu8, 0x32u8, + 0x9bu8, 0xa2u8, 0xa4u8, 0x3cu8, 0x74u8, 0x6au8, 0x08u8, 0x67u8, 0x0eu8, 0x63u8, 0x60u8, + 0xcbu8, 0x46u8, 0x22u8, 0x55u8, 0x43u8, 0x5bu8, 0x84u8, 0x68u8, 0x0fu8, 0x47u8, 0xceu8, + 0x6cu8, 0xd2u8, 0xb8u8, 0xebu8, 0xfeu8, 0xf6u8, 0x9eu8, 0x97u8, 0x0au8, + ]; + + // test the golden path first + let service_id = V3OnionServiceId::from_string(&service_id_string)?; + + let private_key = Ed25519PrivateKey::from_raw(&private_raw)?; + assert_eq!( + private_key, + Ed25519PrivateKey::from_key_blob(&private_key_blob)? + ); + assert_eq!(private_key_blob, private_key.to_key_blob()); + + let public_key = Ed25519PublicKey::from_raw(&public_raw)?; + assert_eq!(public_key, Ed25519PublicKey::from_service_id(&service_id)?); + assert_eq!(public_key, Ed25519PublicKey::from_private_key(&private_key)); + assert_eq!(service_id, V3OnionServiceId::from_public_key(&public_key)); + + let signature = private_key.sign_message(&message); + assert_eq!(signature, Ed25519Signature::from_raw(&signature_raw)?); + assert!(signature.verify(&message, &public_key)); + assert!(!signature.verify(&null_message, &public_key)); + + // some invalid service ids + assert!(!V3OnionServiceId::is_valid("")); + assert!(!V3OnionServiceId::is_valid( + " + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + )); + assert!(!V3OnionServiceId::is_valid( + "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD" + )); + + // generate a new key, get the public key and sign/verify a message + let private_key = Ed25519PrivateKey::generate(); + let public_key = Ed25519PublicKey::from_private_key(&private_key); + let signature = private_key.sign_message(&message); + assert!(signature.verify(&message, &public_key)); + + // test invalid private key blob returns an error + // https://gitlab.torproject.org/tpo/core/arti/-/issues/1021 + let private_raw: [u8; ED25519_PRIVATE_KEY_SIZE] = [ + 0x2eu8, 0x26u8, 0x0au8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x0au8, 0x77u8, 0x77u8, + 0x77u8, 0x77u8, 0x5du8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, + 0x82u8, 0xb4u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, + 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0xffu8, + 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, 0xffu8, + 0xffu8, 0xffu8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x77u8, 0x82u8, 0x88u8, + ]; + match Ed25519PrivateKey::from_raw(&private_raw) { + Ok(_) => panic!("invalid key accepted"), + Err(tor_interface::tor_crypto::Error::KeyInvalid) => (), + Err(err) => panic!("unexpected error: {:?}", err), + } + + Ok(()) +} + +#[test] +fn test_crypto_x25519() -> Result<(), anyhow::Error> { + // private/public key pair + const SECRET_BASE64: &str = "0GeSReJXdNcgvWRQdnDXhJGdu5UiwP2fefgT93/oqn0="; + const SECRET_RAW: [u8; X25519_PRIVATE_KEY_SIZE] = [ + 0xd0u8, 0x67u8, 0x92u8, 0x45u8, 0xe2u8, 0x57u8, 0x74u8, 0xd7u8, 0x20u8, 0xbdu8, 0x64u8, + 0x50u8, 0x76u8, 0x70u8, 0xd7u8, 0x84u8, 0x91u8, 0x9du8, 0xbbu8, 0x95u8, 0x22u8, 0xc0u8, + 0xfdu8, 0x9fu8, 0x79u8, 0xf8u8, 0x13u8, 0xf7u8, 0x7fu8, 0xe8u8, 0xaau8, 0x7du8, + ]; + const PUBLIC_BASE32: &str = "AEXCBCEDJ5KU34YGGMZ7PVHVDEA7D7YB7VQAPJTMTZGRJLN3JASA"; + const PUBLIC_RAW: [u8; X25519_PUBLIC_KEY_SIZE] = [ + 0x01u8, 0x2eu8, 0x20u8, 0x88u8, 0x83u8, 0x4fu8, 0x55u8, 0x4du8, 0xf3u8, 0x06u8, 0x33u8, + 0x33u8, 0xf7u8, 0xd4u8, 0xf5u8, 0x19u8, 0x01u8, 0xf1u8, 0xffu8, 0x01u8, 0xfdu8, 0x60u8, + 0x07u8, 0xa6u8, 0x6cu8, 0x9eu8, 0x4du8, 0x14u8, 0xadu8, 0xbbu8, 0x48u8, 0x24u8, + ]; + + // ensure we can convert from raw as expected + assert_eq!( + &X25519PrivateKey::from_raw(&SECRET_RAW)?.to_base64(), + SECRET_BASE64 + ); + assert_eq!( + &X25519PublicKey::from_raw(&PUBLIC_RAW).to_base32(), + PUBLIC_BASE32 + ); + + // ensure we can round-trip as expected + assert_eq!( + &X25519PrivateKey::from_base64(&SECRET_BASE64)?.to_base64(), + SECRET_BASE64 + ); + assert_eq!( + &X25519PublicKey::from_base32(&PUBLIC_BASE32)?.to_base32(), + PUBLIC_BASE32 + ); + + // ensure we generate the expected public key from private key + let private_key = X25519PrivateKey::from_base64(&SECRET_BASE64)?; + let public_key = X25519PublicKey::from_private_key(&private_key); + assert_eq!(public_key.to_base32(), PUBLIC_BASE32); + + let message = b"All around me are familiar faces"; + + let (signature, signbit) = private_key.sign_message(message)?; + assert!(signature.verify_x25519(message, &public_key, signbit)); + + Ok(()) +} diff --git a/tor-interface/tests/tor_provider.rs b/tor-interface/tests/tor_provider.rs new file mode 100644 index 000000000..206357680 --- /dev/null +++ b/tor-interface/tests/tor_provider.rs @@ -0,0 +1,839 @@ +// stanndard +#[cfg(feature = "legacy-tor-provider")] +use std::fs::File; +use std::io::{Read, Write}; +#[cfg(feature = "legacy-tor-provider")] +use std::ops::Drop; +#[cfg(feature = "legacy-tor-provider")] +use std::process; +#[cfg(feature = "legacy-tor-provider")] +use std::process::{Child, Command, Stdio}; +use std::str::FromStr; +#[cfg(feature = "arti-client-tor-provider")] +use std::sync::Arc; + +// extern crates +use serial_test::serial; +#[cfg(feature = "arti-client-tor-provider")] +use tokio::runtime; + +// internal crates +#[cfg(feature = "arti-client-tor-provider")] +use tor_interface::arti_client_tor_client::*; +#[cfg(feature = "arti-tor-provider")] +use tor_interface::arti_tor_client::*; +#[cfg(feature = "legacy-tor-provider")] +use tor_interface::censorship_circumvention::*; +#[cfg(feature = "legacy-tor-provider")] +use tor_interface::legacy_tor_client::*; +#[cfg(feature = "mock-tor-provider")] +use tor_interface::mock_tor_client::*; +use tor_interface::tor_crypto::*; +use tor_interface::tor_provider::*; + +// +// TorProvider Factory Functions +// + +// purely in-process mock tor provider +#[cfg(test)] +#[cfg(feature = "mock-tor-provider")] +fn build_mock_tor_provider() -> anyhow::Result { + Ok(MockTorClient::new()) +} + +// out-of-process c-tor owned by this process +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_bundled_legacy_tor_provider(name: &str) -> anyhow::Result { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push(name); + + let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: None, + bridge_lines: None, + }; + + Ok(LegacyTorClient::new(tor_config)?) +} + +// out-of-process pt-using c-tor owned by this process +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_bundled_pt_legacy_tor_provider(name: &str) -> anyhow::Result> { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push(name); + + // find the lyrebird bin + let teb_path = std::env::var("TEB_PATH").unwrap_or_default(); + if teb_path.is_empty() { + println!("TEB_PATH environment variable empty, so skipping test_legacy_pluggable_transport_bootstrap()"); + return Ok(None); + } + let mut lyrebird_path = std::path::PathBuf::from(&teb_path); + let lyrebird_bin = format!("lyrebird{}", std::env::consts::EXE_SUFFIX); + lyrebird_path.push(lyrebird_bin.clone()); + assert!(std::path::Path::exists(&lyrebird_path)); + assert!(std::path::Path::is_file(&lyrebird_path)); + + // configure lyrebird pluggable transport + let pluggable_transport = + PluggableTransportConfig::new(vec!["obfs4".to_string()], lyrebird_path)?; + + // obfs4 bridgeline + let bridge_line = BridgeLine::from_str("obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0")?; + + let tor_config = LegacyTorClientConfig::BundledTor { + tor_bin_path: tor_path, + data_directory: data_path, + proxy_settings: None, + allowed_ports: None, + pluggable_transports: Some(vec![pluggable_transport]), + bridge_lines: Some(vec![bridge_line]), + }; + + Ok(Some(LegacyTorClient::new(tor_config)?)) +} + +#[cfg(feature = "legacy-tor-provider")] +struct TorProcess {child: Child} +#[cfg(feature = "legacy-tor-provider")] +impl Drop for TorProcess { + fn drop(&mut self) -> () { + let _ = self.child.kill(); + } +} +#[cfg(feature = "legacy-tor-provider")] +fn build_system_legacy_tor &mut Command>( + name: &str, + control_port: u16, + socks_port: u16, + auth: A +) -> anyhow::Result { + let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?; + + let mut data_path = std::env::temp_dir(); + data_path.push(name); + std::fs::create_dir_all(&data_path)?; + let default_torrc = data_path.join("default_torrc"); + { + let _ = File::create(&default_torrc)?; + } + let torrc = data_path.join("torrc"); + { + let _ = File::create(&torrc)?; + } + + let tor_daemon = TorProcess { child: auth(data_path.clone(), Command::new(tor_path) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .stderr(Stdio::null()) + // point to our above written torrc file + .arg("--defaults-torrc") + .arg(default_torrc) + // location of torrc + .arg("--torrc-file") + .arg(torrc) + // enable networking + .arg("DisableNetwork") + .arg("0") + // root data directory + .arg("DataDirectory") + .arg(data_path) + // control port + .arg("ControlPort") + .arg(control_port.to_string()) + // socks port + .arg("SocksPort") + .arg(socks_port.to_string()) + // tor process will shut down after this process shuts down + // to avoid orphaned tor daemon + .arg("__OwningControllerProcess") + .arg(process::id().to_string())) + .spawn()? + }; + // give daemons time to start + std::thread::sleep(std::time::Duration::from_secs(5)); + Ok(tor_daemon) +} + +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_system_legacy_tor_provider_password( + name: &str, + control_port: u16, + socks_port: u16, +) -> anyhow::Result<(LegacyTorClient, TorProcess)> { + let tor_daemon = build_system_legacy_tor(name, control_port, socks_port, |_, cmd| + // password: foobar1 + cmd.arg("HashedControlPassword") + .arg("16:E807DCE69AFE9979600760C9758B95ADB2F95E8740478AEA5356C95358") + )?; + + let tor_config = LegacyTorClientConfig::SystemTor { + tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?.into(), + tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?.into(), + tor_control_auth: Some(TorAuth::Password("password".to_string())), + }; + let tor_provider = LegacyTorClient::new(tor_config)?; + + Ok((tor_provider, tor_daemon)) +} + +#[cfg(test)] +#[cfg(feature = "legacy-tor-provider")] +fn build_system_legacy_tor_provider_cookie( + name: &str, + control_port: u16, + socks_port: u16, +) -> anyhow::Result<(LegacyTorClient, TorProcess)> { + let mut cookiefile = std::path::PathBuf::new(); + let tor_daemon = build_system_legacy_tor(name, control_port, socks_port, |data_dir, cmd| { + cookiefile = data_dir.join("cookie"); + cmd.arg("CookieAuthentication") + .arg("1") + .arg("CookieAuthFile") + .arg(&cookiefile) + })?; + + let tor_config = LegacyTorClientConfig::SystemTor { + tor_socks_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{socks_port}").as_str())?.into(), + tor_control_addr: std::net::SocketAddr::from_str(format!("127.0.0.1:{control_port}").as_str())?.into(), + tor_control_auth: Some(TorAuth::Cookie(cookiefile)), + }; + let tor_provider = LegacyTorClient::new(tor_config)?; + + Ok((tor_provider, tor_daemon)) +} + +#[cfg(test)] +#[cfg(feature = "arti-client-tor-provider")] +fn build_arti_client_tor_provider(runtime: Arc, name: &str) -> anyhow::Result { + let mut data_path = std::env::temp_dir(); + data_path.push(name); + Ok(ArtiClientTorClient::new(runtime, &data_path)?) +} + +#[cfg(test)] +#[cfg(feature = "arti-tor-provider")] +fn build_arti_tor_provider(name: &str) -> anyhow::Result { + let arti_path = which::which(format!("arti{}", std::env::consts::EXE_SUFFIX))?; + let mut data_path = std::env::temp_dir(); + data_path.push(name); + + let arti_config = ArtiTorClientConfig::BundledArti { + arti_bin_path: arti_path, + data_directory: data_path, + }; + + Ok(ArtiTorClient::new(arti_config)?) +} +// +// Test Functions +// + +#[allow(dead_code)] +pub(crate) fn bootstrap_test(mut tor: P, skip_connect_tests: bool) -> anyhow::Result<()> { + tor.bootstrap()?; + + let mut received_log = false; + let mut bootstrap_complete = false; + while !bootstrap_complete { + for event in tor.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Bootstrap Complete!"); + bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + received_log = true; + println!("--- {}", line); + } + _ => {} + } + } + } + assert!( + received_log, + "should have received a log line from tor provider" + ); + + // + // Attempt to connect to various endpoints + // + + if !skip_connect_tests { + + // example.com + let stream = tor.connect(TargetAddr::from_str("www.example.com:80")?, None)?; + println!("stream: {stream:?}"); + + // google dns (ipv4) + let stream = tor.connect(TargetAddr::from_str("8.8.8.8:53")?, None)?; + println!("stream: {stream:?}"); + + // google dns (ipv6) + let stream = tor.connect(TargetAddr::from_str("[2001:4860:4860::8888]:53")?, None)?; + println!("stream: {stream:?}"); + + // riseup onion service + let stream = tor.connect(TargetAddr::from_str("vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion:80")?, None)?; + println!("stream: {stream:?}"); + + } + + Ok(()) +} + +#[allow(dead_code)] +pub(crate) fn basic_onion_service_test( + mut server_provider: P1, + mut client_provider: P2, +) -> anyhow::Result<()> { + server_provider.bootstrap()?; + client_provider.bootstrap()?; + + let mut server_provider_bootstrap_complete = false; + let mut client_provider_bootstrap_complete = false; + + while !server_provider_bootstrap_complete || !client_provider_bootstrap_complete { + for event in server_provider.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "Server Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Server Provider Bootstrap Complete!"); + server_provider_bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + + for event in client_provider.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "Client Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Client Provider Bootstrap Complete!"); + client_provider_bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } + + // vanilla V3 onion service + { + let tor = &mut server_provider; + + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + println!("Starting and listening to onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = tor.listener(&private_key, VIRT_PORT, None, None)?; + + let mut onion_published = false; + while !onion_published { + for event in tor.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!("Onion Service {} published", service_id.to_string()); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let tor = &mut client_provider; + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service"); + let mut attempt_count = 0; + let mut client = loop { + match tor.connect((service_id.clone(), VIRT_PORT).into(), None) { + Ok(client) => break client, + Err(err) => { + println!("connect error: {:?}", err); + attempt_count += 1; + if attempt_count == 3 { + panic!("failed to connect :("); + } + } + } + }; + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert_eq!(MESSAGE, msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + Ok(()) +} + +#[allow(dead_code)] +pub(crate) fn authenticated_onion_service_test( + mut server_provider: P1, + mut client_provider: P2, +) -> anyhow::Result<()> { + server_provider.bootstrap()?; + client_provider.bootstrap()?; + + let mut server_provider_bootstrap_complete = false; + let mut client_provider_bootstrap_complete = false; + + while !server_provider_bootstrap_complete || !client_provider_bootstrap_complete { + for event in server_provider.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "Server Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Server Provider Bootstrap Complete!"); + server_provider_bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + + for event in client_provider.update()?.iter() { + match event { + TorEvent::BootstrapStatus { + progress, + tag, + summary, + } => println!( + "Client Provider BootstrapStatus: {{ progress: {}, tag: {}, summary: '{}' }}", + progress, tag, summary + ), + TorEvent::BootstrapComplete => { + println!("Client Provider Bootstrap Complete!"); + client_provider_bootstrap_complete = true; + } + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + _ => {} + } + } + } + + // authenticated onion service + { + // create an onion service for this test + let private_key = Ed25519PrivateKey::generate(); + + let private_auth_key = X25519PrivateKey::generate(); + let public_auth_key = X25519PublicKey::from_private_key(&private_auth_key); + + println!("Starting and listening to authenticated onion service"); + const VIRT_PORT: u16 = 42069u16; + let listener = + server_provider.listener(&private_key, VIRT_PORT, Some(&[public_auth_key]), None)?; + + let mut onion_published = false; + while !onion_published { + for event in server_provider.update()?.iter() { + match event { + TorEvent::LogReceived { line } => { + println!("--- {}", line); + } + TorEvent::OnionServicePublished { service_id } => { + let expected_service_id = V3OnionServiceId::from_private_key(&private_key); + if expected_service_id == *service_id { + println!( + "Authenticated Onion Service {} published", + service_id.to_string() + ); + onion_published = true; + } + } + _ => {} + } + } + } + + const MESSAGE: &str = "Hello World!"; + + { + let service_id = V3OnionServiceId::from_private_key(&private_key); + + println!("Connecting to onion service (should fail)"); + assert!( + client_provider + .connect((service_id.clone(), VIRT_PORT).into(), None) + .is_err(), + "should not able to connect to an authenticated onion service without auth key" + ); + + println!("Add auth key for onion service"); + client_provider.add_client_auth(&service_id, &private_auth_key)?; + + println!("Connecting to onion service with authentication"); + let mut attempt_count = 0; + let mut client = loop { + match client_provider.connect((service_id.clone(), VIRT_PORT).into(), None) { + Ok(client) => break client, + Err(err) => { + println!("connect error: {:?}", err); + attempt_count += 1; + if attempt_count == 3 { + panic!("failed to connect :("); + } + } + } + }; + + println!("Client writing message: '{}'", MESSAGE); + client.write_all(MESSAGE.as_bytes())?; + client.flush()?; + println!("End of client scope"); + + println!("Remove auth key for onion service"); + client_provider.remove_client_auth(&service_id)?; + } + + if let Some(mut server) = listener.accept()? { + println!("Server reading message"); + let mut buffer = Vec::new(); + server.read_to_end(&mut buffer)?; + let msg = String::from_utf8(buffer)?; + + assert!(MESSAGE == msg); + println!("Message received: '{}'", msg); + } else { + panic!("no listener"); + } + } + Ok(()) +} + +// +// Mock TorProvider tests +// + +#[test] +#[cfg(feature = "mock-tor-provider")] +fn test_mock_bootstrap() -> anyhow::Result<()> { + bootstrap_test(build_mock_tor_provider()?, true) +} + +#[test] +#[cfg(feature = "mock-tor-provider")] +fn test_mock_onion_service() -> anyhow::Result<()> { + let server_provider = build_mock_tor_provider()?; + let client_provider = build_mock_tor_provider()?; + basic_onion_service_test(server_provider, client_provider) +} + +#[test] +#[cfg(feature = "mock-tor-provider")] +fn test_mock_authenticated_onion_service() -> anyhow::Result<()> { + let server_provider = build_mock_tor_provider()?; + let client_provider = build_mock_tor_provider()?; + authenticated_onion_service_test(server_provider, client_provider) +} + +// +// Legacy TorProvider tests +// + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_legacy_bootstrap() -> anyhow::Result<()> { + let tor_provider = build_bundled_legacy_tor_provider("test_legacy_bootstrap")?; + bootstrap_test(tor_provider, false) +} + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_legacy_pluggable_transport_bootstrap() -> anyhow::Result<()> { + let tor_provider = build_bundled_pt_legacy_tor_provider("test_legacy_pluggable_transport_bootstrap")?; + + if let Some(tor_provider) = tor_provider { + bootstrap_test(tor_provider, false)? + } + Ok(()) +} + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_legacy_onion_service() -> anyhow::Result<()> { + let server_provider = build_bundled_legacy_tor_provider( + "test_legacy_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider( + "test_legacy_onion_service_client")?; + + basic_onion_service_test(server_provider, client_provider) +} + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_legacy_authenticated_onion_service() -> anyhow::Result<()> { + let server_provider = build_bundled_legacy_tor_provider("test_legacy_authenticated_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider("test_legacy_authenticated_onion_service_client")?; + + authenticated_onion_service_test(server_provider, client_provider) +} + +// +// System Legacy TorProvider tests +// + + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_system_legacy_onion_service() -> anyhow::Result<()> { + for (backend, name) in [build_system_legacy_tor_provider_password, build_system_legacy_tor_provider_cookie].into_iter().zip(["password", "cookie"]) { + let server_provider = backend( + &format!("test_system_legacy_onion_service_server_{}", name), + 9251u16, + 9250u16)?; + + let client_provider = backend( + &format!("test_system_legacy_onion_service_client_{}", name), + 9351u16, + 9350u16)?; + + basic_onion_service_test(server_provider.0, client_provider.0)?; + } + + Ok(()) +} + +#[test] +#[serial] +#[cfg(feature = "legacy-tor-provider")] +fn test_system_legacy_authenticated_onion_service() -> anyhow::Result<()> { + for (backend, name) in [build_system_legacy_tor_provider_password, build_system_legacy_tor_provider_cookie].into_iter().zip(["password", "cookie"]) { + let server_provider = backend( + &format!("test_system_legacy_authenticated_onion_service_server_{}", name), + 9251u16, + 9250u16)?; + + let client_provider = backend( + &format!("test_system_legacy_authenticated_onion_service_client_{}", name), + 9351u16, + 9350u16)?; + + authenticated_onion_service_test(server_provider.0, client_provider.0)?; + } + + Ok(()) +} + +// +// Arti-Client TorProvider tests +// + +#[test] +#[serial] +#[cfg(feature = "arti-client-tor-provider")] +fn test_arti_client_bootstrap() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let tor_provider = build_arti_client_tor_provider(runtime, "test_arti_client_bootstrap")?; + bootstrap_test(tor_provider, false) +} + +#[test] +#[cfg(feature = "arti-client-tor-provider")] +fn test_arti_client_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let server_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_basic_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_basic_onion_service_client")?; + + basic_onion_service_test(server_provider, client_provider) +} + +#[test] +#[serial] +#[cfg(feature = "arti-client-tor-provider")] +fn test_arti_client_authenticated_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let server_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_authenticated_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime.clone(), "test_arti_authenticated_onion_service_client")?; + + authenticated_onion_service_test(server_provider, client_provider) +} + +// +// Arti TorProvider tests +// + +#[test] +#[serial] +#[cfg(feature = "arti-tor-provider")] +fn test_arti_bootstrap() -> anyhow::Result<()> { + let tor_provider = build_arti_tor_provider("test_arti_bootstrap")?; + bootstrap_test(tor_provider, false) +} + +// +// Mixed Arti-Client/Legacy TorProvider tests +// + +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_arti_client_legacy_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let server_provider = build_arti_client_tor_provider(runtime, "test_mixed_arti_client_legacy_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_client_legacy_onion_service_client")?; + + basic_onion_service_test(server_provider, client_provider) +} + +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_legacy_arti_client_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_client_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime, "test_mixed_legacy_arti_client_onion_service_client")?; + + basic_onion_service_test(server_provider, client_provider) +} + +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_arti_client_legacy_authenticated_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let server_provider = build_arti_client_tor_provider(runtime, "test_mixed_arti_client_legacy_authenticated_onion_service_server")?; + let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_client_legacy_authenticated_onion_service_client")?; + + authenticated_onion_service_test(server_provider, client_provider) +} + +#[test] +#[serial] +#[cfg(all(feature = "arti-client-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_legacy_arti_client_authenticated_onion_service() -> anyhow::Result<()> { + let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + + let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_client_authenticated_onion_service_server")?; + let client_provider = build_arti_client_tor_provider(runtime, "test_mixed_legacy_arti_client_authenticated_onion_service_client")?; + + authenticated_onion_service_test(server_provider, client_provider) +} + +// +// Mixed Arti/Legacy TorProvider tests +// + +// #[test] +// #[serial] +// #[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +// fn test_mixed_arti_legacy_onion_service() -> anyhow::Result<()> { +// let server_provider = build_arti_tor_provider("test_mixed_arti_legacy_onion_service_server")?; +// let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_legacy_onion_service_client")?; + +// basic_onion_service_test(server_provider, client_provider) +// } + +#[test] +#[serial] +#[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +fn test_mixed_legacy_arti_onion_service() -> anyhow::Result<()> { + let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_onion_service_server")?; + let client_provider = build_arti_tor_provider("test_mixed_legacy_arti_onion_service_client")?; + + basic_onion_service_test(server_provider, client_provider) +} + +// #[test] +// #[serial] +// #[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +// fn test_mixed_arti_legacy_authenticated_onion_service() -> anyhow::Result<()> { +// let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + +// let server_provider = build_arti_tor_provider("test_mixed_arti_legacy_authenticated_onion_service_server")?; +// let client_provider = build_bundled_legacy_tor_provider("test_mixed_arti_legacy_authenticated_onion_service_client")?; + +// authenticated_onion_service_test(server_provider, client_provider) +// } + +// #[test] +// #[serial] +// #[cfg(all(feature = "arti-tor-provider", feature = "legacy-tor-provider"))] +// fn test_mixed_legacy_arti_authenticated_onion_service() -> anyhow::Result<()> { +// let runtime: Arc = Arc::new(runtime::Runtime::new().unwrap()); + +// let server_provider = build_bundled_legacy_tor_provider("test_mixed_legacy_arti_authenticated_onion_service_server")?; +// let client_provider = build_arti_tor_provider("test_mixed_legacy_arti_authenticated_onion_service_client")?; + +// authenticated_onion_service_test(server_provider, client_provider) +// } diff --git a/tor-interface/tests/tor_utils.rs b/tor-interface/tests/tor_utils.rs new file mode 100644 index 000000000..0cb9e6253 --- /dev/null +++ b/tor-interface/tests/tor_utils.rs @@ -0,0 +1,196 @@ +// std +use std::str::FromStr; + +// internal crates +use tor_interface::tor_provider::*; + +// +// Misc Utils +// + +#[test] +fn test_tor_provider_target_addr() -> anyhow::Result<()> { + let valid_ip_addr: &[&str] = &[ + "192.168.1.1:80", + "10.0.0.1:443", + "172.16.0.1:8080", + "8.8.8.8:53", + "255.255.255.255:65535", + "0.0.0.0:22", + "192.168.0.254:21", + "127.0.0.1:3306", + "1.1.1.1:123", + "224.0.0.1:554", + "169.254.0.1:179", + "203.0.113.1:80", + "198.51.100.1:443", + "100.64.0.1:8080", + "192.0.2.1:53", + "192.88.99.1:22", + "192.0.0.1:21", + "240.0.0.1:3306", + "198.18.0.1:123", + "233.252.0.1:554", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", + "[2001:db8:85a3::8a2e:370:7334]:443", + "[::1]:8080", + "[::ffff:192.168.1.1]:53", + "[2001:0db8::1]:22", + "[fe80::1ff:fe23:4567:890a]:21", + "[2001:db8::1:0:0:1]:3306", + "[2001:0db8:0000:0042:0000:8a2e:0370:7334]:123", + "[ff02::1]:554", + "[fe80::abcd:ef01:2345:6789]:179", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80", + "[2001:db8:85a3::8a2e:370:7334]:443", + "[::1]:8080", + "[::ffff:c0a8:101]:53", + "[2001:db8::1:0:0:1]:22", + "[fe80::1ff:fe23:4567:890a]:21", + "[2001:db8:0000:0042:0000:8a2e:0370:7334]:3306", + "[ff02::1]:123", + "[fe80::abcd:ef01:2345:6789]:554", + "[2001:db8::1]:179", + ]; + + for target_addr_str in valid_ip_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => println!("{} => {}", target_addr_str, socket_addr), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(err) => Err(err)?, + } + } + + let valid_onion_addr: &[&str] = &[ + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:65535", + "6L62FW7TQCTLU5FESDQUKVPOXEZKAXBZLLRAFA2VE6EWUHZPHXCZSJYD.onion:1", + ]; + + for target_addr_str in valid_onion_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => panic!( + "unexpected conversion: {} => Ip({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => { + println!("{} => {}", target_addr_str, onion_addr) + } + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(err) => Err(err)?, + } + } + + let valid_domain_addr: &[&str] = &[ + "example.com:80", + "subdomain.example.com:443", + "xn--e1afmkfd.xn--p1ai:8080", // domain in Punycode for "пример.рф" + "xn--fsqu00a.xn--0zwm56d:53", // domain in Punycode for "例子.测试" + "münich.com:22", // domain with UTF-8 characters + "xn--mnich-kva.com:21", // Punycode for "münich.com" + "exämple.com:3306", // domain with UTF-8 characters + "xn--exmple-cua.com:123", // Punycode for "exämple.com" + "例子.com:554", // domain with UTF-8 characters + "xn--fsqu00a.com:179", // Punycode for "例子.com" + "täst.de:80", // domain with UTF-8 characters + "xn--tst-qla.de:443", // Punycode for "täst.de" + "xn--fiqs8s:80", // Punycode for "中国" + "xn--wgbh1c:8080", // Punycode for "مصر" + "münster.de:22", // domain with UTF-8 characters + "xn--mnster-3ya.de:21", // Punycode for "münster.de" + "bücher.com:3306", // domain with UTF-8 characters + "xn--bcher-kva.com:123", // Punycode for "bücher.com" + "xn--vermgensberatung-pwb.com:554", // Punycode for "vermögensberatung.com" + // Max Length + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:65535" + ]; + + for target_addr_str in valid_domain_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => panic!( + "unexpected conversion: {} => SocketAddr({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => { + println!("{} => {}", target_addr_str, domain_addr) + } + Err(err) => Err(err)?, + } + } + + let invalid_target_addr: &[&str] = &[ + // ipv4-ish + "192.168.1.1:99999", // Port number out of range + "192.168.1.1:abc", // Invalid port number + "192.168.1.1:", // Missing port number + "192.168.1.1: 80", // Space in port number + "192.168.1.1:80a", // Non-numeric characters in port number + // ipv6-ish + "[2001:db8:::1]:80", // Triple colons + "[2001:db8:85a3::8a2e:370:7334:1234::abcd]:80", // Too many groups + "[2001:db8:85a3::8a2e:370g:7334]:80", // Invalid character in group + "[2001:db8:85a3::8a2e:370:7334]:99999", // Port number out of range + "[2001:db8:85a3:8a2e:370:7334]:80", // Missing double colons + "[::12345]:80", // Excessive leading zeroes + "[2001:db8:85a3::8a2e:370:7334:]:80", // Trailing colon + "[2001:db8:85a3::8a2e:370:7334]", // Missing port number + "2001:db8:85a3::8a2e:370:7334:80", // Missing square brackets + "[2001:db8:85a3::8a2e:370:7334]: 80", // Space in port number + "[2001:db8:85a3::8a2e:370:7334]:80a", // Non-numeric characters in port number + // onion service-ish + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd234567.onion:80", // Too long for v3 + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxcz.onion:443", // Too short for v3 + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:99999", // Port number out of range + "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrst.onion:21", // Invalid characters + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:abc", // Invalid port number + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion: 80", // Space in port number + "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd.onion:80a", // Non-numeric characters in port number + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.onion:80", // Invalid service id + // domain-ish + "example..com:80", // Double dots + "exa mple.com:53", // Space in domain + "example.com:99999", // Port number out of range + "exaample.com:abc", // Invalid port number + "exaample.com:", // Missing port number + "exaample.com: 80", // Space in port number + "ex@mple.com:80", // Special character in domain + "example.com:80a", // Non-numeric characters in port number + "exämple..com:80", // UTF-8 with double dot + "xn--exmple-cua.com: 80", // Punycode with space in port number + "xn--exmple-cua.com:80a", // Punycode with non-numeric port + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com:65535", // Label too long + ]; + + for target_addr_str in invalid_target_addr { + match TargetAddr::from_str(target_addr_str) { + Ok(TargetAddr::Socket(socket_addr)) => panic!( + "unexpected conversion: {} => SocketAddr({})", + target_addr_str, socket_addr + ), + Ok(TargetAddr::OnionService(onion_addr)) => panic!( + "unexpected conversion: {} => OnionService({})", + target_addr_str, onion_addr + ), + Ok(TargetAddr::Domain(domain_addr)) => panic!( + "unexpected conversion: {} => DomainAddr({})", + target_addr_str, domain_addr + ), + Err(_) => (), + } + } + + Ok(()) +}