diff options
| author | David Lönnhager <david.l@mullvad.net> | 2023-05-03 11:20:31 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2023-05-03 11:20:31 +0200 |
| commit | 49ea114adddba1a1db6ffc6c440e743c01797a47 (patch) | |
| tree | 66f1bf1e3e1d208e233e5622045503abe85a3a89 | |
| parent | beaa6d3b80d9c9dfed99c710c793830db3ddc7ec (diff) | |
| parent | aade46c9c73c874e4153caa450e713d8f8b37760 (diff) | |
| download | mullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.tar.xz mullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.zip | |
Merge branch 'update-clap'
61 files changed, 3634 insertions, 3484 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c26d7bedfc..ddbd6861c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,15 @@ Line wrap the file at 100 chars. Th ### Changed - Update Electron from 21.1.1 to 23.2.0. +- In the CLI, update the `tunnel` subcommand to resemble `relay` more. For example, by adding a + unified `mullvad tunnel get` command and removing individual `get` subcommands like + `mullvad tunnel ipv6 get`. +- Update the CLI multihop settings to make it possible to set the entry location without toggling + multihop on or off. + +#### Windows +- In the CLI, add a unified `mullvad split-tunnel get` command to replace the old commands + `mullvad split-tunnel pid list` and `mullvad split-tunnel get`. #### Android - Clarify some of the error messages showed in notifications. diff --git a/Cargo.lock b/Cargo.lock index eb4e901e38..3a1659baf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,55 @@ dependencies = [ ] [[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] name = "anyhow" version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -130,7 +179,7 @@ checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -141,7 +190,7 @@ checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -331,7 +380,6 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" dependencies = [ - "clap", "heck", "indexmap", "log", @@ -339,7 +387,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn", + "syn 1.0.100", "tempfile", "toml", ] @@ -415,39 +463,57 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" dependencies = [ - "atty", + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" +dependencies = [ + "anstream", + "anstyle", "bitflags", "clap_lex", - "indexmap", "once_cell", "strsim 0.10.0", - "termcolor", - "textwrap", ] [[package]] name = "clap_complete" -version = "3.0.6" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678db4c39c013cc68b54d372bce2efc58e30a0337c497c9032fd196802df3bc3" +checksum = "1a19591b2ab0e3c04b588a0e04ddde7b9eaa423646d1b4a8092879216bf47473" dependencies = [ "clap", ] [[package]] -name = "clap_lex" -version = "0.2.4" +name = "clap_derive" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ - "os_str_bytes", + "heck", + "proc-macro2", + "quote", + "syn 2.0.15", ] [[package]] +name = "clap_lex" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + +[[package]] name = "classic-mceliece-rust" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -459,6 +525,12 @@ dependencies = [ ] [[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] name = "colored" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -598,7 +670,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.9.3", - "syn", + "syn 1.0.100", ] [[package]] @@ -609,7 +681,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -647,7 +719,7 @@ checksum = "302ccf094df1151173bb6f5a2282fcd2f45accd5eae1bdf82dcbfefbc501ad5c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -660,7 +732,7 @@ dependencies = [ "derive_builder_core", "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -672,7 +744,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -685,7 +757,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.100", ] [[package]] @@ -808,7 +880,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -850,7 +922,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.100", "synstructure", ] @@ -866,6 +938,17 @@ dependencies = [ ] [[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -988,7 +1071,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -1120,12 +1203,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "hex" @@ -1407,14 +1487,14 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.2" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi 0.3.1", "io-lifetimes", - "rustix", - "windows-sys 0.42.0", + "rustix 0.37.3", + "windows-sys 0.48.0", ] [[package]] @@ -1479,7 +1559,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -1542,6 +1622,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] +name = "linux-raw-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" + +[[package]] name = "lock_api" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1727,12 +1813,12 @@ dependencies = [ name = "mullvad-cli" version = "0.0.0" dependencies = [ + "anyhow", "base64 0.13.0", "chrono", "clap", "clap_complete", "env_logger 0.10.0", - "err-derive", "futures", "itertools", "mullvad-management-interface", @@ -1948,6 +2034,7 @@ name = "mullvad-types" version = "0.0.0" dependencies = [ "chrono", + "clap", "err-derive", "ipnetwork", "jnix", @@ -2201,12 +2288,6 @@ dependencies = [ ] [[package]] -name = "os_str_bytes" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" - -[[package]] name = "p256" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2304,7 +2385,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -2335,7 +2416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52063325d6b0de17051e72275d44f96c5b73a75029fcdd7e05e54a62ff216437" dependencies = [ "derive_builder", - "errno", + "errno 0.2.8", "error-chain", "ioctl-sys", "ipnetwork", @@ -2397,7 +2478,7 @@ checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -2474,7 +2555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a49e86d2c26a24059894a3afa13fd17d063419b05dfb83f06d9c3566060c3f5a" dependencies = [ "proc-macro2", - "syn", + "syn 1.0.100", ] [[package]] @@ -2486,7 +2567,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.100", "version_check", ] @@ -2503,9 +2584,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -2550,7 +2631,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -2596,14 +2677,14 @@ checksum = "b22a693222d716a9587786f37ac3f6b4faedb5b80c23914e7303ff5a1d8016e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -2812,14 +2893,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" dependencies = [ "bitflags", - "errno", + "errno 0.2.8", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.4", "windows-sys 0.42.0", ] [[package]] +name = "rustix" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b24138615de35e32031d041a09032ef3487a616d901ca4db224e7d557efae2" +dependencies = [ + "bitflags", + "errno 0.3.1", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.6", + "windows-sys 0.45.0", +] + +[[package]] name = "rustls" version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2935,7 +3030,7 @@ checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -3221,6 +3316,17 @@ dependencies = [ ] [[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] name = "sync_wrapper" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3234,7 +3340,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", "unicode-xid", ] @@ -3547,7 +3653,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix", + "rustix 0.36.7", "windows-sys 0.42.0", ] @@ -3561,12 +3667,6 @@ dependencies = [ ] [[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - -[[package]] name = "thiserror" version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3583,7 +3683,7 @@ checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -3658,7 +3758,7 @@ checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -3765,7 +3865,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -3840,7 +3940,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", ] [[package]] @@ -4080,6 +4180,12 @@ dependencies = [ ] [[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4154,7 +4260,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.100", "wasm-bindgen-shared", ] @@ -4176,7 +4282,7 @@ checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4291,12 +4397,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] @@ -4306,7 +4412,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", ] [[package]] @@ -4315,22 +4430,43 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4343,6 +4479,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4355,6 +4497,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4367,6 +4515,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4379,12 +4533,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4397,6 +4563,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] name = "winreg" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4442,6 +4614,6 @@ checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.100", "synstructure", ] diff --git a/mullvad-cli/Cargo.toml b/mullvad-cli/Cargo.toml index 2ba23a65ca..ab3e0ac03f 100644 --- a/mullvad-cli/Cargo.toml +++ b/mullvad-cli/Cargo.toml @@ -12,17 +12,17 @@ name = "mullvad" path = "src/main.rs" [dependencies] +anyhow = "1.0" base64 = "0.13" chrono = { version = "0.4.19", features = ["serde"] } -clap = { version = "3.0", features = ["cargo"] } -err-derive = "0.3.1" +clap = { version = "4.2.7", features = ["cargo", "derive"] } env_logger = "0.10.0" futures = "0.3" natord = "1.0.9" serde = "1.0" itertools = "0.10" -mullvad-types = { path = "../mullvad-types" } +mullvad-types = { path = "../mullvad-types", features = ["clap"] } mullvad-paths = { path = "../mullvad-paths" } mullvad-version = { path = "../mullvad-version" } talpid-types = { path = "../talpid-types" } @@ -31,7 +31,7 @@ mullvad-management-interface = { path = "../mullvad-management-interface" } tokio = { version = "1.8", features = [ "rt-multi-thread" ] } [target.'cfg(all(unix, not(target_os = "android")))'.dependencies] -clap_complete = { version = "3.0" } +clap_complete = { version = "4.2.1" } [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/mullvad-cli/src/cmds/account.rs b/mullvad-cli/src/cmds/account.rs index 77859990ea..d46469484f 100644 --- a/mullvad-cli/src/cmds/account.rs +++ b/mullvad-cli/src/cmds/account.rs @@ -1,183 +1,131 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::{anyhow, Result}; +use clap::Subcommand; use itertools::Itertools; -use mullvad_management_interface::{ - types::{self, Timestamp}, - Code, ManagementServiceClient, Status, -}; -use mullvad_types::{account::AccountToken, device::Device}; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{account::AccountToken, device::DeviceState}; use std::io::{self, Write}; const NOT_LOGGED_IN_MESSAGE: &str = "Not logged in on any account"; const REVOKED_MESSAGE: &str = "The current device has been revoked"; -const DEVICE_NOT_FOUND_ERROR: &str = "There is no such device"; -const INVALID_ACCOUNT_ERROR: &str = "The account does not exist"; -const TOO_MANY_DEVICES_ERROR: &str = - "There are too many devices on this account. Revoke one to log in"; -const ALREADY_LOGGED_IN_ERROR: &str = - "You are already logged in. Please log out before creating a new account"; -pub struct Account; +#[derive(Subcommand, Debug)] +pub enum Account { + /// Create and log in on a new account + Create, -#[mullvad_management_interface::async_trait] -impl Command for Account { - fn name(&self) -> &'static str { - "account" - } + /// Log in on an account + Login { + /// The Mullvad account token to configure the client with + account: Option<String>, + }, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control and display information about your Mullvad account") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("create").about("Create and log in to a new account")) - .subcommand( - clap::App::new("login").about("Log in to an account").arg( - clap::Arg::new("account") - .help("The Mullvad account token to configure the client with") - .required(false), - ), - ) - .subcommand(clap::App::new("logout").about("Log out of the current account")) - .subcommand( - clap::App::new("get") - .about("Display information about the current account") - .arg( - clap::Arg::new("verbose") - .long("verbose") - .short('v') - .help("Enables verbose output"), - ), - ) - .subcommand( - clap::App::new("list-devices") - .about("List devices associated with an account") - .arg( - clap::Arg::new("account") - .help("Mullvad account number") - .long("account") - .takes_value(true), - ) - .arg( - clap::Arg::new("verbose") - .long("verbose") - .short('v') - .help("Enables verbose output"), - ), - ) - .subcommand( - clap::App::new("revoke-device") - .about("Revoke a device associated with an account") - .arg( - clap::Arg::new("account") - .help("Mullvad account number") - .long("account") - .takes_value(true), - ) - .arg( - clap::Arg::new("device") - .help("Name or ID of the device to revoke") - .required(true), - ), - ) - .subcommand( - clap::App::new("redeem").about("Redeems a voucher").arg( - clap::Arg::new("voucher") - .help("The Mullvad voucher code to be submitted") - .required(true), - ), - ) - } + /// Log out of the current account + Logout, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(_matches) = matches.subcommand_matches("create") { - self.create().await - } else if let Some(set_matches) = matches.subcommand_matches("login") { - self.login(parse_token_else_stdin(set_matches)).await - } else if let Some(_matches) = matches.subcommand_matches("logout") { - self.logout().await - } else if let Some(set_matches) = matches.subcommand_matches("get") { - let verbose = set_matches.is_present("verbose"); - self.get(verbose).await - } else if let Some(set_matches) = matches.subcommand_matches("list-devices") { - self.list_devices(set_matches).await - } else if let Some(set_matches) = matches.subcommand_matches("revoke-device") { - self.revoke_device(set_matches).await - } else if let Some(matches) = matches.subcommand_matches("redeem") { - let voucher = matches.value_of_t_or_exit("voucher"); - self.redeem_voucher(voucher).await - } else { - unreachable!("No account command given"); - } - } + /// Display information about the current account + Get { + /// Enable verbose output + #[arg(long, short = 'v')] + verbose: bool, + }, + + /// List devices associated with an account + ListDevices { + /// Mullvad account number (current account if not specified) + #[arg(long, short = 'a')] + account: Option<String>, + + /// Enable verbose output + #[arg(long, short = 'v')] + verbose: bool, + }, + + /// Revoke a device associated with an account + RevokeDevice { + /// Name or UID of the device to revoke + device: String, + + /// Mullvad account number (current account if not specified) + #[arg(long, short = 'a')] + account: Option<String>, + }, + + /// Redeem a voucher + Redeem { + /// Voucher code to submit + voucher: String, + }, } impl Account { - async fn create(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.create_new_account(()).await.map_err(map_device_error)?; + pub async fn handle(self) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + match self { + Account::Create => Self::create(&mut rpc).await, + Account::Login { account } => { + Self::login( + &mut rpc, + account.unwrap_or_else(|| from_stdin("Enter an account number: ")), + ) + .await + } + Account::Logout => Self::logout(&mut rpc).await, + Account::Get { verbose } => Self::get(&mut rpc, verbose).await, + Account::ListDevices { account, verbose } => { + Self::list_devices(&mut rpc, account, verbose).await + } + Account::RevokeDevice { device, account } => { + Self::revoke_device(&mut rpc, device, account).await + } + Account::Redeem { voucher } => Self::redeem_voucher(&mut rpc, voucher).await, + } + } + + async fn create(rpc: &mut MullvadProxyClient) -> Result<()> { + rpc.create_new_account().await?; println!("New account created!"); - self.get(false).await + Self::get(rpc, false).await } - async fn login(&self, token: AccountToken) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.login_account(token.clone()) - .await - .map_err(map_device_error)?; + async fn login(rpc: &mut MullvadProxyClient, token: AccountToken) -> Result<()> { + rpc.login_account(token.clone()).await?; println!("Mullvad account \"{token}\" set"); Ok(()) } - async fn logout(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.logout_account(()).await?; + async fn logout(rpc: &mut MullvadProxyClient) -> Result<()> { + rpc.logout_account().await?; println!("Removed device from Mullvad account"); Ok(()) } - async fn get(&self, verbose: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; + async fn get(rpc: &mut MullvadProxyClient, verbose: bool) -> Result<()> { + let _ = rpc.update_device().await; - let _ = rpc.update_device(()).await; + let state = rpc.get_device().await?; - let state = rpc - .get_device(()) - .await - .map_err(map_device_error)? - .into_inner(); - - use types::device_state::State; - - match State::from_i32(state.state).unwrap() { - State::LoggedIn => { - let device = state.device.expect("Device must be provided if logged in"); + match state { + DeviceState::LoggedIn(device) => { println!("Mullvad account: {}", device.account_token); - let inner_device = Device::try_from(device.device.unwrap()).unwrap(); - println!("Device name : {}", inner_device.pretty_name()); + println!("Device name : {}", device.device.pretty_name()); if verbose { - println!("Device id : {}", inner_device.id); - println!("Device pubkey : {}", inner_device.pubkey); - println!( - "Device created : {}", - inner_device.created.with_timezone(&chrono::Local) - ); - for port in inner_device.ports { + println!("Device id : {}", device.device.id); + println!("Device pubkey : {}", device.device.pubkey); + println!("Device created : {}", device.device.created,); + for port in device.device.ports { println!("Device port : {port}"); } } - let expiry = rpc - .get_account_data(device.account_token) - .await - .map_err(|error| Error::RpcFailedExt("Failed to fetch account data", error))? - .into_inner(); + let expiry = rpc.get_account_data(device.account_token).await?; println!( "Expires at : {}", - Self::format_expiry(&expiry.expiry.unwrap()) + expiry.expiry.with_timezone(&chrono::Local), ); } - State::LoggedOut => { + DeviceState::LoggedOut => { println!("{NOT_LOGGED_IN_MESSAGE}"); } - State::Revoked => { + DeviceState::Revoked => { println!("{REVOKED_MESSAGE}"); } } @@ -185,23 +133,17 @@ impl Account { Ok(()) } - async fn list_devices(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let token = self.parse_account_else_current(&mut rpc, matches).await?; - let mut device_list = rpc - .list_devices(token) - .await - .map_err(map_device_error)? - .into_inner(); - - let verbose = matches.is_present("verbose"); + async fn list_devices( + rpc: &mut MullvadProxyClient, + account: Option<String>, + verbose: bool, + ) -> Result<()> { + let token = account_else_current(rpc, account).await?; + let mut device_list = rpc.list_devices(token).await?; println!("Devices on the account:"); - device_list - .devices - .sort_unstable_by_key(|dev| dev.created.as_ref().map(|dt| dt.seconds).unwrap_or(0)); - for device in device_list.devices { - let device = Device::try_from(device.clone()).unwrap(); + device_list.sort_unstable_by_key(|dev| dev.created.timestamp()); + for device in device_list { if verbose { println!(); println!("Name : {}", device.pretty_name()); @@ -222,146 +164,80 @@ impl Account { Ok(()) } - async fn revoke_device(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; + async fn revoke_device( + rpc: &mut MullvadProxyClient, + device: String, + account: Option<String>, + ) -> Result<()> { + let token = account_else_current(rpc, account).await?; - let token = self.parse_account_else_current(&mut rpc, matches).await?; - let device_to_revoke = parse_device_name(matches); - - let device_list = rpc - .list_devices(token.clone()) - .await - .map_err(map_device_error)? - .into_inner(); + let device_list = rpc.list_devices(token.clone()).await?; let device_id = device_list - .devices .into_iter() .find(|dev| { - dev.name.eq_ignore_ascii_case(&device_to_revoke) - || dev.id.eq_ignore_ascii_case(&device_to_revoke) + dev.name.eq_ignore_ascii_case(&device) || dev.id.eq_ignore_ascii_case(&device) }) .map(|dev| dev.id) - .ok_or(Error::Other(DEVICE_NOT_FOUND_ERROR))?; + .ok_or(mullvad_management_interface::Error::DeviceNotFound)?; - rpc.remove_device(types::DeviceRemoval { - account_token: token, - device_id, - }) - .await - .map_err(map_device_error)?; + rpc.remove_device(token, device_id).await?; println!("Removed device"); Ok(()) } - async fn parse_account_else_current( - &self, - rpc: &mut ManagementServiceClient, - matches: &clap::ArgMatches, - ) -> Result<String> { - match matches.value_of("account").map(str::to_string) { - Some(token) => Ok(token), - None => { - let state = rpc - .get_device(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain device", error))? - .into_inner(); - if state.state != types::device_state::State::LoggedIn as i32 { - return Err(Error::Other("Log in or specify an account")); - } - Ok(state.device.unwrap().account_token) - } - } - } - - async fn redeem_voucher(&self, mut voucher: String) -> Result<()> { - let mut rpc = new_rpc_client().await?; + async fn redeem_voucher(rpc: &mut MullvadProxyClient, mut voucher: String) -> Result<()> { voucher.retain(|c| c.is_alphanumeric()); - match rpc.submit_voucher(voucher).await { - Ok(submission) => { - let submission = submission.into_inner(); - println!( - "Added {} to the account", - Self::format_duration(submission.seconds_added) - ); - println!( - "New expiry date: {}", - Self::format_expiry(&submission.new_expiry.unwrap()) - ); - Ok(()) - } - Err(err) => { - match err.code() { - Code::NotFound | Code::ResourceExhausted => { - eprintln!("Failed to submit voucher: {}", err.message()); - } - _ => return Err(Error::RpcFailed(err)), - } - std::process::exit(1); - } - } - } - - fn format_duration(seconds: u64) -> String { - let dur = chrono::Duration::seconds(seconds as i64); - if dur.num_days() > 0 { - format!("{} days", dur.num_days()) - } else if dur.num_hours() > 0 { - format!("{} hours", dur.num_hours()) - } else if dur.num_minutes() > 0 { - format!("{} minutes", dur.num_minutes()) - } else { - format!("{} seconds", dur.num_seconds()) - } - } - - fn format_expiry(expiry: &Timestamp) -> String { - let ndt = chrono::NaiveDateTime::from_timestamp(expiry.seconds, expiry.nanos as u32); - let utc = chrono::DateTime::<chrono::Utc>::from_utc(ndt, chrono::Utc); - utc.with_timezone(&chrono::Local).to_string() + let submission = rpc.submit_voucher(voucher).await?; + println!( + "Added {} to the account", + format_duration(submission.time_added) + ); + println!( + "New expiry date: {}", + submission.new_expiry.with_timezone(&chrono::Local), + ); + Ok(()) } } -fn map_device_error(error: Status) -> Error { - match error.code() { - Code::ResourceExhausted => Error::Other(TOO_MANY_DEVICES_ERROR), - Code::Unauthenticated => Error::Other(INVALID_ACCOUNT_ERROR), - Code::AlreadyExists => Error::Other(ALREADY_LOGGED_IN_ERROR), - Code::NotFound => Error::Other(DEVICE_NOT_FOUND_ERROR), - _other => Error::RpcFailed(error), +async fn account_else_current( + rpc: &mut MullvadProxyClient, + token: Option<String>, +) -> Result<String> { + match token { + Some(account) => Ok(account), + None => { + let state = rpc.get_device().await?; + match state { + DeviceState::LoggedIn(account) => Ok(account.account_token), + _ => Err(anyhow!("Log in or specify an account")), + } + } } } -fn parse_token_else_stdin(matches: &clap::ArgMatches) -> String { - parse_from_match_else_stdin("Enter account number: ", "account", matches) - .split_whitespace() - .join("") -} - -fn parse_device_name(matches: &clap::ArgMatches) -> String { - parse_from_match_else_stdin("Enter device name: ", "device", matches) - .trim() - .to_string() +fn from_stdin(prompt_str: &'static str) -> String { + let mut val = String::new(); + io::stdout() + .write_all(prompt_str.as_bytes()) + .expect("Failed to write to STDOUT"); + let _ = io::stdout().flush(); + io::stdin() + .read_line(&mut val) + .expect("Failed to read from STDIN"); + val.split_whitespace().join("") } -fn parse_from_match_else_stdin( - prompt_str: &'static str, - key: &'static str, - matches: &clap::ArgMatches, -) -> String { - match matches.value_of(key) { - Some(device) => device.to_string(), - None => { - let mut val = String::new(); - io::stdout() - .write_all(prompt_str.as_bytes()) - .expect("Failed to write to STDOUT"); - let _ = io::stdout().flush(); - io::stdin() - .read_line(&mut val) - .expect("Failed to read from STDIN"); - val - } +fn format_duration(seconds: u64) -> String { + let dur = chrono::Duration::seconds(seconds as i64); + if dur.num_days() > 0 { + format!("{} days", dur.num_days()) + } else if dur.num_hours() > 0 { + format!("{} hours", dur.num_hours()) + } else if dur.num_minutes() > 0 { + format!("{} minutes", dur.num_minutes()) + } else { + format!("{} seconds", dur.num_seconds()) } } diff --git a/mullvad-cli/src/cmds/auto_connect.rs b/mullvad-cli/src/cmds/auto_connect.rs index 067dff9a6b..4fe578f820 100644 --- a/mullvad-cli/src/cmds/auto_connect.rs +++ b/mullvad-cli/src/cmds/auto_connect.rs @@ -1,53 +1,36 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct AutoConnect; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for AutoConnect { - fn name(&self) -> &'static str { - "auto-connect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control the daemon auto-connect setting") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about("Change auto-connect setting") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand(clap::App::new("get").about("Display the current auto-connect setting")) - } +#[derive(Subcommand, Debug)] +pub enum AutoConnect { + /// Display the current auto-connect setting + Get, + /// Change auto-connect setting + Set { policy: BooleanOption }, +} - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - let auto_connect = set_matches.value_of("policy").expect("missing policy"); - self.set(auto_connect == "on").await - } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get().await - } else { - unreachable!("No auto-connect command given"); +impl AutoConnect { + pub async fn handle(self) -> Result<()> { + match self { + AutoConnect::Get => Self::get().await, + AutoConnect::Set { policy } => Self::set(policy).await, } } -} -impl AutoConnect { - async fn set(&self, auto_connect: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_auto_connect(auto_connect).await?; + async fn set(policy: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_auto_connect(*policy).await?; println!("Changed auto-connect setting"); Ok(()) } - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let auto_connect = rpc.get_settings(()).await?.into_inner().auto_connect; - println!("Autoconnect: {}", if auto_connect { "on" } else { "off" }); + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let auto_connect = BooleanOption::from(rpc.get_settings().await?.auto_connect); + println!("Autoconnect: {auto_connect}"); Ok(()) } } diff --git a/mullvad-cli/src/cmds/beta_program.rs b/mullvad-cli/src/cmds/beta_program.rs index 77aa73ace3..ba9c72616e 100644 --- a/mullvad-cli/src/cmds/beta_program.rs +++ b/mullvad-cli/src/cmds/beta_program.rs @@ -1,61 +1,43 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::{anyhow, Result}; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct BetaProgram; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for BetaProgram { - fn name(&self) -> &'static str { - "beta-program" - } +#[derive(Subcommand, Debug)] +pub enum BetaProgram { + /// Get beta notifications setting + Get, + /// Change beta notifications setting + Set { policy: BooleanOption }, +} - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Receive notifications about beta updates") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about("Change beta notifications setting") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand(clap::App::new("get").about("Get beta notifications setting")) +impl BetaProgram { + pub async fn handle(self) -> Result<()> { + match self { + BetaProgram::Get => Self::get().await, + BetaProgram::Set { policy } => Self::set(policy).await, + } } - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("get", _)) => { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - let enabled_str = if settings.show_beta_releases { - "on" - } else { - "off" - }; - println!("Beta program: {enabled_str}"); - Ok(()) - } - Some(("set", matches)) => { - let enable_str = matches.value_of("policy").expect("missing policy"); - let enable = enable_str == "on"; + async fn set(state: BooleanOption) -> Result<()> { + if !*state && mullvad_version::VERSION.contains("beta") { + return Err(anyhow!( + "The beta program must be enabled while running a beta version", + )); + } - if !enable && mullvad_version::VERSION.contains("beta") { - return Err(Error::InvalidCommand( - "The beta program must be enabled while running a beta version", - )); - } + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_show_beta_releases(*state).await?; - let mut rpc = new_rpc_client().await?; - rpc.set_show_beta_releases(enable).await?; + println!("Beta program: {state}"); + Ok(()) + } - println!("Beta program: {enable_str}"); - Ok(()) - } - _ => { - unreachable!("unhandled command"); - } - } + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let opt = BooleanOption::from(rpc.get_settings().await?.show_beta_releases); + println!("Beta program: {opt}"); + Ok(()) } } diff --git a/mullvad-cli/src/cmds/block_when_disconnected.rs b/mullvad-cli/src/cmds/block_when_disconnected.rs deleted file mode 100644 index 36b9a20e37..0000000000 --- a/mullvad-cli/src/cmds/block_when_disconnected.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::{new_rpc_client, Command, Result}; - -pub struct BlockWhenDisconnected; - -#[mullvad_management_interface::async_trait] -impl Command for BlockWhenDisconnected { - fn name(&self) -> &'static str { - "lockdown-mode" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control if the system service should block network access when disconnected from VPN") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about("Change the lockdown mode setting") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand( - clap::App::new("get") - .about("Display the current lockdown mode setting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - let block_when_disconnected = set_matches.value_of("policy").expect("missing policy"); - self.set(block_when_disconnected == "on").await - } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get().await - } else { - unreachable!("No block-when-disconnected command given"); - } - } -} - -impl BlockWhenDisconnected { - async fn set(&self, block_when_disconnected: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_block_when_disconnected(block_when_disconnected) - .await?; - println!("Changed lockdown mode setting"); - Ok(()) - } - - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let block_when_disconnected = rpc - .get_settings(()) - .await? - .into_inner() - .block_when_disconnected; - println!( - "Network traffic will be {} when the VPN is disconnected", - if block_when_disconnected { - "blocked" - } else { - "allowed" - } - ); - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/bridge.rs b/mullvad-cli/src/cmds/bridge.rs index d7291a810d..1652a0cfb2 100644 --- a/mullvad-cli/src/cmds/bridge.rs +++ b/mullvad-cli/src/cmds/bridge.rs @@ -1,224 +1,238 @@ -use crate::{location, new_rpc_client, Command, Error, Result}; - -use mullvad_management_interface::types; -use mullvad_types::relay_constraints::{ - BridgeConstraints, BridgeSettings, BridgeState, Constraint, LocationConstraint, +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{ + relay_constraints::{ + BridgeConstraints, BridgeSettings, BridgeState, Constraint, LocationConstraint, Ownership, + Provider, Providers, + }, + relay_list::RelayEndpointData, }; +use std::net::{IpAddr, SocketAddr}; use talpid_types::net::openvpn::{self, SHADOWSOCKS_CIPHERS}; -use std::{convert::TryFrom, net::SocketAddr}; +use super::relay_constraints::LocationArgs; -pub struct Bridge; +#[derive(Subcommand, Debug)] +pub enum Bridge { + /// Get current bridge settings + Get, + /// Set bridge state and settings, such as provider + #[clap(subcommand)] + Set(SetCommands), + /// List available bridge relays + List, +} -#[mullvad_management_interface::async_trait] -impl Command for Bridge { - fn name(&self) -> &'static str { - "bridge" - } +#[derive(Subcommand, Debug, Clone)] +pub enum SetCommands { + /// Specify whether to use a bridge + State { policy: BridgeState }, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about( - "Manage use of bridges, socks proxies and Shadowsocks for OpenVPN. \ - Can make OpenVPN tunnels use Shadowsocks via one of the Mullvad bridge servers. \ - Can also make OpenVPN connect through any custom SOCKS5 proxy. \ - These settings also affect how the app reaches the API over Shadowsocks.", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_bridge_set_subcommand()) - .subcommand(clap::App::new("get").about("Get current bridge settings and state")) - .subcommand(clap::App::new("list").about("List bridge relays")) - } + /// Set country or city to select relays from. Use the 'list' + /// command to show available alternatives. + Location(LocationArgs), - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("set", set_matches)) => Self::handle_set(set_matches).await, - Some(("get", _)) => Self::handle_get().await, - Some(("list", _)) => Self::list_bridge_relays().await, - _ => unreachable!("unhandled command"), - } - } -} + /// Set hosting provider(s) to select relays from. The 'list' + /// command shows the available relays and their providers. + Provider { + /// Either 'any', or provider to select from. + #[arg(required(true), num_args = 1..)] + providers: Vec<Provider>, + }, -fn create_bridge_set_subcommand() -> clap::App<'static> { - clap::App::new("set") - .about("Set bridge state and settings") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_set_state_subcommand()) - .subcommand(create_set_custom_settings_subcommand()) - .subcommand( - clap::App::new("provider") - .about( - "Set hosting provider(s) to select bridge relays from. The 'list' \ - command shows the available relays and their providers.", - ) - .arg( - clap::Arg::new("provider") - .help("The hosting provider(s) to use, or 'any' for no preference.") - .multiple_values(true) - .required(true), - ), - ) - .subcommand( - clap::App::new("ownership") - .about( - "Filters bridges based on ownership. The 'list' \ - command shows the available relays and whether they're rented.", - ) - .arg( - clap::Arg::new("ownership") - .help("Ownership preference, or 'any' for no preference.") - .possible_values(["any", "owned", "rented"]) - .required(true), - ), - ) - .subcommand(location::get_subcommand().about( - "Set country or city to select bridge relays from. Use the 'list' \ - command to show available alternatives.", - )) + /// Filter relays based on ownership. The 'list' command + /// shows the available relays and whether they're rented. + Ownership { + /// Servers to select from: 'any', 'owned', or 'rented'. + ownership: Constraint<Ownership>, + }, + + /// Configure a SOCKS5 proxy + #[clap(subcommand)] + Custom(SetCustomCommands), } -fn create_set_custom_settings_subcommand() -> clap::App<'static> { - #[allow(unused_mut)] - let mut local_subcommand = clap::App::new("local") - .about("Registers a local SOCKS5 proxy") - .arg( - clap::Arg::new("local-port") - .help("Specifies the port the local proxy server is listening on") - .required(true) - .index(1), +#[derive(Subcommand, Debug, Clone)] +pub enum SetCustomCommands { + /// Configure a local SOCKS5 proxy + #[cfg_attr( + target_os = "linux", + clap( + about = "Registers a local SOCKS5 proxy. The server must be excluded using \ + 'mullvad-exclude', or `SO_MARK` must be set to '0x6d6f6c65', in order \ + to bypass firewall restrictions" ) - .arg( - clap::Arg::new("remote-ip") - .help("Specifies the IP of the proxy server peer") - .required(true) - .index(2), + )] + #[cfg_attr( + target_os = "windows", + clap( + about = "Registers a local SOCKS5 proxy. The server must be excluded using \ + split tunneling in order to bypass firewall restrictions" ) - .arg( - clap::Arg::new("remote-port") - .help("Specifies the port of the proxy server peer") - .required(true) - .index(3), - ); + )] + #[cfg_attr( + target_os = "macos", + clap( + about = "Registers a local SOCKS5 proxy. The server must run as root to bypass \ + firewall restrictions" + ) + )] + Local { + /// The port that the server on localhost is listening on + local_port: u16, + /// The IP of the remote peer + remote_ip: IpAddr, + /// The port of the remote peer + remote_port: u16, + }, - #[cfg(target_os = "linux")] - { - local_subcommand = local_subcommand.about( - "Registers a local SOCKS5 proxy. The server must be excluded using \ - 'mullvad-exclude', or `SO_MARK` must be set to '0x6d6f6c65', in order \ - to bypass firewall restrictions", - ); - } - #[cfg(target_os = "macos")] - { - local_subcommand = local_subcommand.about( - "Registers a local SOCKS5 proxy. The server must run as root to bypass \ - firewall restrictions", - ); - } + /// Configure a remote SOCKS5 proxy + Remote { + /// The IP of the remote proxy server + remote_ip: IpAddr, + /// The port of the remote proxy server + remote_port: u16, - clap::App::new("custom") - .about("Configure a SOCKS5 proxy") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(local_subcommand) - .subcommand( - clap::App::new("remote") - .about("Registers a remote SOCKS5 proxy") - .arg( - clap::Arg::new("remote-ip") - .help("Specifies the IP of the remote proxy server") - .required(true) - .index(1), - ) - .arg( - clap::Arg::new("remote-port") - .help("Specifies the port the remote proxy server is listening on") - .required(true) - .index(2), - ) - .arg( - clap::Arg::new("username") - .help("Specifies the username for remote authentication") - .required(true) - .index(3), - ) - .arg( - clap::Arg::new("password") - .help("Specifies the password for remote authentication") - .required(true) - .index(4), - ), - ) - .subcommand( - clap::App::new("shadowsocks") - .about("Configure bundled Shadowsocks proxy") - .arg( - clap::Arg::new("remote-ip") - .help("Specifies the IP of the remote Shadowsocks server") - .required(true) - .index(1), - ) - .arg( - clap::Arg::new("remote-port") - .help("Specifies the port of the remote Shadowsocks server") - .default_value("443") - .index(2), - ) - .arg( - clap::Arg::new("password") - .help("Specifies the password on the remote Shadowsocks server") - .default_value("mullvad") - .index(3), - ) - .arg( - clap::Arg::new("cipher") - .help("Specifies the cipher to use") - .default_value("aes-256-gcm") - .possible_values(SHADOWSOCKS_CIPHERS) - .index(4), - ), - ) -} + /// Username for authentication + #[arg(requires = "password")] + username: Option<String>, + /// Password for authentication + #[arg(requires = "username")] + password: Option<String>, + }, + + /// Configure bundled Shadowsocks proxy + Shadowsocks { + /// The IP of the remote Shadowsocks server + remote_ip: IpAddr, + /// The port of the remote Shadowsocks server + #[arg(default_value = "443")] + remote_port: u16, -fn create_set_state_subcommand() -> clap::App<'static> { - clap::App::new("state").about("Set bridge state").arg( - clap::Arg::new("policy") - .help("Specifies whether a bridge should be used") - .required(true) - .index(1) - .possible_values(["auto", "on", "off"]), - ) + /// Password for authentication + #[arg(default_value = "mullvad")] + password: String, + + /// Cipher to use + #[arg(value_parser = SHADOWSOCKS_CIPHERS, default_value = "aes-256-gcm")] + cipher: String, + }, } impl Bridge { - async fn handle_set(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("location", location_matches)) => { - Self::handle_set_bridge_location(location_matches).await + pub async fn handle(self) -> Result<()> { + match self { + Bridge::Get => Self::get().await, + Bridge::List => Self::list().await, + Bridge::Set(subcmd) => Self::set(subcmd).await, + } + } + + async fn set(subcmd: SetCommands) -> Result<()> { + match subcmd { + SetCommands::State { policy } => { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_state(policy).await?; + println!("Updated bridge state"); + Ok(()) + } + SetCommands::Location(location) => { + Self::update_bridge_settings(Some(Constraint::from(location)), None, None).await } - Some(("provider", provider_matches)) => { - Self::handle_set_bridge_provider(provider_matches).await + SetCommands::Ownership { ownership } => { + Self::update_bridge_settings(None, None, Some(ownership)).await + } + SetCommands::Provider { providers } => { + let providers = if providers[0].eq_ignore_ascii_case("any") { + Constraint::Any + } else { + Constraint::Only(Providers::new(providers.into_iter()).unwrap()) + }; + Self::update_bridge_settings(None, Some(providers), None).await + } + SetCommands::Custom(subcmd) => Self::set_custom(subcmd).await, + } + } + + async fn set_custom(subcmd: SetCustomCommands) -> Result<()> { + match subcmd { + SetCustomCommands::Local { + local_port, + remote_ip, + remote_port, + } => { + let local_proxy = openvpn::LocalProxySettings { + port: local_port, + peer: SocketAddr::new(remote_ip, remote_port), + }; + let packed_proxy = openvpn::ProxySettings::Local(local_proxy); + if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { + panic!("{}", error); + } + + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy)) + .await?; } - Some(("ownership", ownership_matches)) => { - Self::handle_set_bridge_ownership(ownership_matches).await + SetCustomCommands::Remote { + remote_ip, + remote_port, + username, + password, + } => { + let auth = match (username, password) { + (Some(username), Some(password)) => { + Some(openvpn::ProxyAuth { username, password }) + } + _ => None, + }; + let proxy = openvpn::RemoteProxySettings { + address: SocketAddr::new(remote_ip, remote_port), + auth, + }; + let packed_proxy = openvpn::ProxySettings::Remote(proxy); + if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { + panic!("{}", error); + } + + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy)) + .await?; } - Some(("custom", custom_matches)) => { - Self::handle_bridge_set_custom_settings(custom_matches).await + SetCustomCommands::Shadowsocks { + remote_ip, + remote_port, + password, + cipher, + } => { + let proxy = openvpn::ShadowsocksProxySettings { + peer: SocketAddr::new(remote_ip, remote_port), + password, + cipher, + #[cfg(target_os = "linux")] + fwmark: None, + }; + let packed_proxy = openvpn::ProxySettings::Shadowsocks(proxy); + if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { + panic!("{}", error); + } + + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy)) + .await?; } - Some(("state", set_matches)) => Self::handle_set_bridge_state(set_matches).await, - _ => unreachable!("unhandled command"), } + + println!("Updated bridge settings"); + Ok(()) } - async fn handle_get() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - let bridge_settings = BridgeSettings::try_from(settings.bridge_settings.unwrap()).unwrap(); - println!( - "Bridge state: {}", - BridgeState::try_from(settings.bridge_state.unwrap()).unwrap() - ); - match bridge_settings { + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + println!("Bridge state: {}", settings.bridge_state); + match settings.bridge_settings { BridgeSettings::Custom(proxy) => match proxy { openvpn::ProxySettings::Local(local_proxy) => Self::print_local_proxy(&local_proxy), openvpn::ProxySettings::Remote(remote_proxy) => { @@ -235,175 +249,6 @@ impl Bridge { Ok(()) } - async fn handle_set_bridge_location(matches: &clap::ArgMatches) -> Result<()> { - Self::update_bridge_settings( - Some(location::get_constraint_from_args(matches)), - None, - None, - ) - .await - } - - async fn handle_set_bridge_provider(matches: &clap::ArgMatches) -> Result<()> { - let providers: Vec<String> = matches.values_of_t_or_exit("provider"); - let providers = if providers.get(0).map(String::as_str) == Some("any") { - vec![] - } else { - providers - }; - - Self::update_bridge_settings(None, Some(providers), None).await - } - - async fn handle_set_bridge_ownership(matches: &clap::ArgMatches) -> Result<()> { - let ownership = - super::relay::parse_ownership_constraint(matches.value_of("ownership").unwrap()); - Self::update_bridge_settings(None, None, Some(ownership)).await - } - - async fn update_bridge_settings( - location: Option<types::RelayLocation>, - providers: Option<Vec<String>>, - ownership: Option<types::Ownership>, - ) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - - let bridge_settings = BridgeSettings::try_from(settings.bridge_settings.unwrap()).unwrap(); - let constraints = match bridge_settings { - BridgeSettings::Normal(mut constraints) => { - if let Some(new_location) = location { - constraints.location = Constraint::<LocationConstraint>::from(new_location); - } - if let Some(new_providers) = providers { - constraints.providers = - types::relay_constraints::try_providers_constraint_from_proto( - &new_providers, - ) - .unwrap(); - } - if let Some(new_ownership) = ownership { - constraints.ownership = - types::relay_constraints::ownership_constraint_from_proto(new_ownership); - } - constraints - } - _ => { - let location = Constraint::<LocationConstraint>::from(location.unwrap_or_default()); - let providers = types::relay_constraints::try_providers_constraint_from_proto( - &providers.unwrap_or_default(), - ) - .unwrap(); - let ownership = ownership - .map(types::relay_constraints::ownership_constraint_from_proto) - .unwrap_or_default(); - - BridgeConstraints { - location, - providers, - ownership, - } - } - }; - - rpc.set_bridge_settings( - types::BridgeSettings::try_from(BridgeSettings::Normal(constraints)).unwrap(), - ) - .await?; - Ok(()) - } - - async fn handle_set_bridge_state(matches: &clap::ArgMatches) -> Result<()> { - let state = match matches.value_of("policy").unwrap() { - "auto" => BridgeState::Auto, - "on" => BridgeState::On, - "off" => BridgeState::Off, - _ => unreachable!(), - }; - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_state(types::BridgeState::from(state)) - .await?; - Ok(()) - } - - async fn handle_bridge_set_custom_settings(matches: &clap::ArgMatches) -> Result<()> { - if let Some(args) = matches.subcommand_matches("local") { - let local_port = args.value_of_t_or_exit("local-port"); - let remote_ip = args.value_of_t_or_exit("remote-ip"); - let remote_port = args.value_of_t_or_exit("remote-port"); - - let local_proxy = openvpn::LocalProxySettings { - port: local_port, - peer: SocketAddr::new(remote_ip, remote_port), - }; - let packed_proxy = openvpn::ProxySettings::Local(local_proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { - panic!("{}", error); - } - - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_settings(types::BridgeSettings::from(BridgeSettings::Custom( - packed_proxy, - ))) - .await?; - } else if let Some(args) = matches.subcommand_matches("remote") { - let remote_ip = args.value_of_t_or_exit("remote-ip"); - let remote_port = args.value_of_t_or_exit("remote-port"); - let username = args.value_of("username"); - let password = args.value_of("password"); - - let auth = match (username, password) { - (Some(username), Some(password)) => Some(openvpn::ProxyAuth { - username: username.to_string(), - password: password.to_string(), - }), - _ => None, - }; - let proxy = openvpn::RemoteProxySettings { - address: SocketAddr::new(remote_ip, remote_port), - auth, - }; - let packed_proxy = openvpn::ProxySettings::Remote(proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { - panic!("{}", error); - } - - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_settings(types::BridgeSettings::from(BridgeSettings::Custom( - packed_proxy, - ))) - .await?; - } else if let Some(args) = matches.subcommand_matches("shadowsocks") { - let remote_ip = args.value_of_t_or_exit("remote-ip"); - let remote_port = args.value_of_t_or_exit("remote-port"); - let password = args.value_of_t_or_exit("password"); - let cipher = args.value_of_t_or_exit("cipher"); - - let proxy = openvpn::ShadowsocksProxySettings { - peer: SocketAddr::new(remote_ip, remote_port), - password, - cipher, - #[cfg(target_os = "linux")] - fwmark: None, - }; - let packed_proxy = openvpn::ProxySettings::Shadowsocks(proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { - panic!("{}", error); - } - - let mut rpc = new_rpc_client().await?; - rpc.set_bridge_settings(types::BridgeSettings::from(BridgeSettings::Custom( - packed_proxy, - ))) - .await?; - } else { - unreachable!("unhandled proxy type"); - } - - println!("proxy details have been updated"); - Ok(()) - } - fn print_local_proxy(proxy: &openvpn::LocalProxySettings) { println!("proxy: local"); println!(" local port: {}", proxy.port); @@ -429,13 +274,9 @@ impl Bridge { println!(" cipher: {}", proxy.cipher); } - async fn list_bridge_relays() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let relay_list = rpc - .get_relay_locations(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain relay locations", error))? - .into_inner(); + async fn list() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let relay_list = rpc.get_relay_locations().await?; let mut countries = Vec::new(); @@ -445,8 +286,7 @@ impl Bridge { .into_iter() .filter_map(|mut city| { city.relays.retain(|relay| { - relay.active - && relay.endpoint_type == (types::relay::RelayType::Bridge as i32) + relay.active && matches!(relay.endpoint_data, RelayEndpointData::Bridge) }); if !city.relays.is_empty() { Some(city) @@ -489,4 +329,39 @@ impl Bridge { } Ok(()) } + + async fn update_bridge_settings( + location: Option<Constraint<LocationConstraint>>, + providers: Option<Constraint<Providers>>, + ownership: Option<Constraint<Ownership>>, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let constraints = match rpc.get_settings().await?.bridge_settings { + BridgeSettings::Normal(mut constraints) => { + if let Some(new_location) = location { + constraints.location = new_location; + } + if let Some(new_providers) = providers { + constraints.providers = new_providers; + } + if let Some(new_ownership) = ownership { + constraints.ownership = new_ownership; + } + constraints + } + _ => BridgeConstraints { + location: location.unwrap_or(Constraint::Any), + providers: providers.unwrap_or(Constraint::Any), + ownership: ownership.unwrap_or(Constraint::Any), + }, + }; + + rpc.set_bridge_settings(BridgeSettings::Normal(constraints)) + .await?; + + println!("Updated bridge settings"); + + Ok(()) + } } diff --git a/mullvad-cli/src/cmds/connect.rs b/mullvad-cli/src/cmds/connect.rs deleted file mode 100644 index 0f470d3d2a..0000000000 --- a/mullvad-cli/src/cmds/connect.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{format, new_rpc_client, state, Command, Error, Result}; -use futures::StreamExt; -use mullvad_types::states::TunnelState; - -pub struct Connect; - -#[mullvad_management_interface::async_trait] -impl Command for Connect { - fn name(&self) -> &'static str { - "connect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Command the client to start establishing a VPN tunnel") - .arg( - clap::Arg::new("wait") - .long("wait") - .short('w') - .help("Wait until connected before exiting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - - let receiver_option = if matches.is_present("wait") { - Some(state::state_listen(rpc.clone())) - } else { - None - }; - - if rpc.connect_tunnel(()).await?.into_inner() { - if let Some(mut receiver) = receiver_option { - while let Some(state) = receiver.next().await { - let state = state?; - format::print_state(&state, false); - match state { - TunnelState::Connected { .. } => return Ok(()), - TunnelState::Error(_) => return Err(Error::CommandFailed("connect")), - _ => {} - } - } - return Err(Error::StatusListenerFailed); - } - } - - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/disconnect.rs b/mullvad-cli/src/cmds/disconnect.rs deleted file mode 100644 index 4ea5722fe9..0000000000 --- a/mullvad-cli/src/cmds/disconnect.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{format, new_rpc_client, state, Command, Error, Result}; -use futures::StreamExt; - -pub struct Disconnect; - -#[mullvad_management_interface::async_trait] -impl Command for Disconnect { - fn name(&self) -> &'static str { - "disconnect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Command the client to disconnect the VPN tunnel") - .arg( - clap::Arg::new("wait") - .long("wait") - .short('w') - .help("Wait until disconnected before exiting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - - let receiver_option = if matches.is_present("wait") { - Some(state::state_listen(rpc.clone())) - } else { - None - }; - - if rpc.disconnect_tunnel(()).await?.into_inner() { - if let Some(mut receiver) = receiver_option { - while let Some(state) = receiver.next().await { - let state = state?; - format::print_state(&state, false); - if state.is_disconnected() { - return Ok(()); - } - } - return Err(Error::StatusListenerFailed); - } - } - - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/dns.rs b/mullvad-cli/src/cmds/dns.rs index 28a48a5614..fd2b215936 100644 --- a/mullvad-cli/src/cmds/dns.rs +++ b/mullvad-cli/src/cmds/dns.rs @@ -1,161 +1,87 @@ -use crate::{new_rpc_client, Command, Result}; -use mullvad_management_interface::types; -use mullvad_types::settings::{DnsOptions, DnsState}; -use std::{convert::TryInto, net::IpAddr}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::settings::{CustomDnsOptions, DefaultDnsOptions, DnsOptions, DnsState}; +use std::net::IpAddr; -pub struct Dns; +#[derive(Subcommand, Debug)] +pub enum Dns { + /// Display the current DNS settings + Get, -#[mullvad_management_interface::async_trait] -impl Command for Dns { - fn name(&self) -> &'static str { - "dns" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Configure DNS servers to use when connected") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get").about("Display the current DNS settings")) - .subcommand( - clap::App::new("set") - .about("Set DNS servers to use") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("default") - .about("Use default DNS servers") - .arg( - clap::Arg::new("block ads") - .long("block-ads") - .takes_value(false) - .help("Block domain names used for ads"), - ) - .arg( - clap::Arg::new("block trackers") - .long("block-trackers") - .takes_value(false) - .help("Block domain names used for tracking"), - ) - .arg( - clap::Arg::new("block malware") - .long("block-malware") - .takes_value(false) - .help("Block domains known to be used by malware"), - ) - .arg( - clap::Arg::new("block adult content") - .long("block-adult-content") - .takes_value(false) - .help("Block domains known to be used for adult content"), - ) - .arg( - clap::Arg::new("block gambling") - .long("block-gambling") - .takes_value(false) - .help("Block domains known to be used for gambling"), - ), - ) - .subcommand( - clap::App::new("custom") - .about("Set a list of custom DNS servers") - .arg( - clap::Arg::new("servers") - .multiple_occurrences(true) - .help("One or more IP addresses pointing to DNS resolvers.") - .required(true), - ), - ), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("set", matches)) => match matches.subcommand() { - Some(("default", matches)) => { - self.set_default( - matches.is_present("block ads"), - matches.is_present("block trackers"), - matches.is_present("block malware"), - matches.is_present("block adult content"), - matches.is_present("block gambling"), - ) - .await - } - Some(("custom", matches)) => { - let servers = match matches.values_of_t::<IpAddr>("servers") { - Ok(servers) => Some(servers), - Err(e) => match e.kind { - clap::ErrorKind::ArgumentNotFound => None, - _ => e.exit(), - }, - }; - self.set_custom(servers).await - } - _ => unreachable!("No custom-dns server command given"), - }, - Some(("get", _)) => self.get().await, - _ => unreachable!("No custom-dns command given"), - } - } + /// Set DNS servers to use + Set { + #[clap(subcommand)] + cmd: DnsSet, + }, } -impl Dns { - async fn set_default( - &self, +#[derive(Subcommand, Debug, Clone)] +pub enum DnsSet { + /// Use a default DNS server, with or without content + /// blocking. + Default { + /// Block domains known to be used for ads + #[arg(long)] block_ads: bool, + + /// Block domains known to be used for tracking + #[arg(long)] block_trackers: bool, + + /// Block domains known to be used by malware + #[arg(long)] block_malware: bool, + + /// Block domains known to be used for adult content + #[arg(long)] block_adult_content: bool, + + /// Block domains known to be used for gambling + #[arg(long)] block_gambling: bool, - ) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - rpc.set_dns_options(types::DnsOptions { - state: types::dns_options::DnsState::Default as i32, - default_options: Some(types::DefaultDnsOptions { - block_ads, - block_trackers, - block_malware, - block_adult_content, - block_gambling, - }), - ..settings.tunnel_options.unwrap().dns_options.unwrap() - }) - .await?; - println!("Updated DNS settings"); - Ok(()) - } + }, - async fn set_custom(&self, servers: Option<Vec<IpAddr>>) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let settings = rpc.get_settings(()).await?.into_inner(); - rpc.set_dns_options(types::DnsOptions { - state: types::dns_options::DnsState::Custom as i32, - custom_options: Some(types::CustomDnsOptions { - addresses: servers - .unwrap_or_default() - .into_iter() - .map(|a| a.to_string()) - .collect(), - }), - ..settings.tunnel_options.unwrap().dns_options.unwrap() - }) - .await?; - println!("Updated DNS settings"); - Ok(()) + /// Set a list of custom DNS servers + Custom { + /// One or more IP addresses pointing to DNS resolvers + #[arg(required(true), num_args = 1..)] + servers: Vec<IpAddr>, + }, +} + +impl Dns { + pub async fn handle(self) -> Result<()> { + match self { + Dns::Get => Self::get().await, + Dns::Set { + cmd: + DnsSet::Default { + block_ads, + block_trackers, + block_malware, + block_adult_content, + block_gambling, + }, + } => { + Self::set_default( + block_ads, + block_trackers, + block_malware, + block_adult_content, + block_gambling, + ) + .await + } + Dns::Set { + cmd: DnsSet::Custom { servers }, + } => Self::set_custom(servers).await, + } } - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let options: DnsOptions = rpc - .get_settings(()) - .await? - .into_inner() - .tunnel_options - .unwrap() - .dns_options - .unwrap() - .try_into() - .unwrap(); + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let options = rpc.get_settings().await?.tunnel_options.dns_options; match options.state { DnsState::Default => { @@ -179,4 +105,42 @@ impl Dns { Ok(()) } + + async fn set_default( + block_ads: bool, + block_trackers: bool, + block_malware: bool, + block_adult_content: bool, + block_gambling: bool, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + rpc.set_dns_options(DnsOptions { + state: DnsState::Default, + default_options: DefaultDnsOptions { + block_ads, + block_trackers, + block_malware, + block_adult_content, + block_gambling, + }, + ..settings.tunnel_options.dns_options + }) + .await?; + println!("Updated DNS settings"); + Ok(()) + } + + async fn set_custom(servers: Vec<IpAddr>) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?; + rpc.set_dns_options(DnsOptions { + state: DnsState::Custom, + custom_options: CustomDnsOptions { addresses: servers }, + ..settings.tunnel_options.dns_options + }) + .await?; + println!("Updated DNS settings"); + Ok(()) + } } diff --git a/mullvad-cli/src/cmds/lan.rs b/mullvad-cli/src/cmds/lan.rs index d1c3635c43..7bf92063ca 100644 --- a/mullvad-cli/src/cmds/lan.rs +++ b/mullvad-cli/src/cmds/lan.rs @@ -1,56 +1,41 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct Lan; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for Lan { - fn name(&self) -> &'static str { - "lan" - } +#[derive(Subcommand, Debug)] +pub enum Lan { + /// Display the current local network sharing setting + Get, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Control the allow local network sharing setting") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set").about("Change allow LAN setting").arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["allow", "block"]), - ), - ) - .subcommand( - clap::App::new("get").about("Display the current local network sharing setting"), - ) - } + /// Change allow LAN setting + Set { + #[arg(value_parser = BooleanOption::custom_parser("allow", "block"))] + policy: BooleanOption, + }, +} - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - let allow_lan = set_matches.value_of("policy").expect("missing policy"); - self.set(allow_lan == "allow").await - } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get().await - } else { - unreachable!("No lan command given"); +impl Lan { + pub async fn handle(self) -> Result<()> { + match self { + Lan::Get => Self::get().await, + Lan::Set { policy } => Self::set(policy).await, } } -} -impl Lan { - async fn set(&self, allow_lan: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_allow_lan(allow_lan).await?; + async fn set(policy: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_allow_lan(*policy).await?; println!("Changed local network sharing setting"); Ok(()) } - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let allow_lan = rpc.get_settings(()).await?.into_inner().allow_lan; - println!( - "Local network sharing setting: {}", - if allow_lan { "allow" } else { "block" } - ); + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let allow_lan = + BooleanOption::with_labels(rpc.get_settings().await?.allow_lan, "allow", "block"); + println!("Local network sharing setting: {allow_lan}"); Ok(()) } } diff --git a/mullvad-cli/src/cmds/lockdown.rs b/mullvad-cli/src/cmds/lockdown.rs new file mode 100644 index 0000000000..001f195fda --- /dev/null +++ b/mullvad-cli/src/cmds/lockdown.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; + +use super::BooleanOption; + +#[derive(Subcommand, Debug)] +pub enum LockdownMode { + /// Display the current lockdown mode setting + Get, + /// Change the lockdown mode setting + Set { policy: BooleanOption }, +} + +impl LockdownMode { + pub async fn handle(self) -> Result<()> { + match self { + LockdownMode::Get => Self::get().await, + LockdownMode::Set { policy } => Self::set(policy).await, + } + } + + async fn set(policy: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_block_when_disconnected(*policy).await?; + println!("Changed lockdown mode setting"); + Ok(()) + } + + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let state = BooleanOption::from(rpc.get_settings().await?.block_when_disconnected); + println!("Block traffic when the VPN is disconnected: {state}"); + Ok(()) + } +} diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs index 4c374d64ee..7adcf6c65b 100644 --- a/mullvad-cli/src/cmds/mod.rs +++ b/mullvad-cli/src/cmds/mod.rs @@ -1,86 +1,80 @@ -use crate::Command; -use std::collections::HashMap; +use clap::builder::{PossibleValuesParser, TypedValueParser, ValueParser}; +use std::ops::Deref; -mod account; -pub use self::account::Account; +pub mod account; +pub mod auto_connect; +pub mod beta_program; +pub mod bridge; +pub mod dns; +pub mod lan; +pub mod lockdown; +pub mod obfuscation; +pub mod relay; +pub mod relay_constraints; +pub mod reset; +pub mod split_tunnel; +pub mod status; +pub mod tunnel; +pub mod tunnel_state; +pub mod version; -mod auto_connect; -pub use self::auto_connect::AutoConnect; - -mod beta_program; -pub use self::beta_program::BetaProgram; - -mod block_when_disconnected; -pub use self::block_when_disconnected::BlockWhenDisconnected; - -mod bridge; -pub use self::bridge::Bridge; - -mod connect; -pub use self::connect::Connect; - -mod disconnect; -pub use self::disconnect::Disconnect; - -mod dns; -pub use self::dns::Dns; - -mod lan; -pub use self::lan::Lan; +/// A value parser that parses "on" or "off" into a boolean +#[derive(Debug, Clone, Copy)] +pub struct BooleanOption { + state: bool, + on_label: &'static str, + off_label: &'static str, +} -mod obfuscation; -pub use self::obfuscation::Obfuscation; +impl Deref for BooleanOption { + type Target = bool; -mod reconnect; -pub use self::reconnect::Reconnect; + fn deref(&self) -> &Self::Target { + &self.state + } +} -mod relay; -pub use self::relay::Relay; +impl clap::builder::ValueParserFactory for BooleanOption { + type Parser = ValueParser; -mod reset; -pub use self::reset::Reset; + /// A value parser that parses "on" or "off" into a `BooleanOption` + fn value_parser() -> Self::Parser { + Self::custom_parser("on", "off") + } +} -#[cfg(any(target_os = "linux", windows))] -mod split_tunnel; -#[cfg(any(target_os = "linux", windows))] -pub use self::split_tunnel::SplitTunnel; +impl BooleanOption { + /// A value parser that parses `on_label` and `off_label` into a `BooleanOption` + fn custom_parser(on_label: &'static str, off_label: &'static str) -> ValueParser { + assert!(on_label != off_label); -mod status; -pub use self::status::Status; + ValueParser::new( + PossibleValuesParser::new([on_label, off_label]) + .map(move |val| Self::with_labels(val == on_label, on_label, off_label)), + ) + } -mod tunnel; -pub use self::tunnel::Tunnel; + fn with_labels(state: bool, on_label: &'static str, off_label: &'static str) -> Self { + Self { + state, + on_label, + off_label, + } + } +} -mod version; -pub use self::version::Version; +impl From<bool> for BooleanOption { + fn from(state: bool) -> Self { + Self::with_labels(state, "on", "off") + } +} -/// Returns a map of all available subcommands with their name as key. -pub fn get_commands() -> HashMap<&'static str, Box<dyn Command>> { - let commands: Vec<Box<dyn Command>> = vec![ - Box::new(Account), - Box::new(AutoConnect), - Box::new(BetaProgram), - Box::new(BlockWhenDisconnected), - Box::new(Bridge), - Box::new(Connect), - Box::new(Disconnect), - Box::new(Dns), - Box::new(Reconnect), - Box::new(Lan), - Box::new(Obfuscation), - Box::new(Relay), - Box::new(Reset), - #[cfg(any(target_os = "linux", windows))] - Box::new(SplitTunnel), - Box::new(Status), - Box::new(Tunnel), - Box::new(Version), - ]; - let mut map = HashMap::new(); - for cmd in commands { - if map.insert(cmd.name(), cmd).is_some() { - panic!("Multiple commands with the same name"); +impl std::fmt::Display for BooleanOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.state { + self.on_label.fmt(f) + } else { + self.off_label.fmt(f) } } - map } diff --git a/mullvad-cli/src/cmds/obfuscation.rs b/mullvad-cli/src/cmds/obfuscation.rs index 5a8b908340..b2aaaa1f6e 100644 --- a/mullvad-cli/src/cmds/obfuscation.rs +++ b/mullvad-cli/src/cmds/obfuscation.rs @@ -1,137 +1,74 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::relay_constraints::{ + Constraint, ObfuscationSettings, SelectedObfuscation, Udp2TcpObfuscationSettings, +}; -use mullvad_management_interface::{types as grpc_types, ManagementServiceClient}; +#[derive(Subcommand, Debug)] +pub enum Obfuscation { + /// Get current obfuscation settings + Get, -use mullvad_types::relay_constraints::{ObfuscationSettings, SelectedObfuscation}; - -use std::convert::TryFrom; - -pub struct Obfuscation; - -#[mullvad_management_interface::async_trait] -impl Command for Obfuscation { - fn name(&self) -> &'static str { - "obfuscation" - } + /// Set obfuscation settings + #[clap(subcommand)] + Set(SetCommands), +} - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about( - "Manage use of obfuscation protocols for WireGuard. \ - Can make WireGuard traffic look like something else on the network. \ - Helps circumvent censorship and to establish a tunnel when on restricted networks", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_obfuscation_set_subcommand()) - .subcommand(create_obfuscation_get_subcommand()) - } +#[derive(Subcommand, Debug, Clone)] +pub enum SetCommands { + /// Specifies if obfuscation should be used with WireGuard connections. + /// And if so, what obfuscation protocol it should use. + Mode { mode: SelectedObfuscation }, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("set", set_matches)) => Self::handle_set(set_matches).await, - Some(("get", _get_matches)) => Self::handle_get().await, - _ => unreachable!("unhandled command"), - } - } + /// Specifies the config for the udp2tcp obfuscator. + Udp2tcp { + /// Port to use, or 'any' + #[arg(long, short = 'p')] + port: Constraint<u16>, + }, } impl Obfuscation { - async fn handle_set(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("mode", mode_matches)) => { - let obfuscator_type = mode_matches.value_of("mode").unwrap(); - let mut rpc = new_rpc_client().await?; - let mut settings = Self::get_obfuscation_settings(&mut rpc).await?; - settings.selected_obfuscation = match obfuscator_type { - "auto" => SelectedObfuscation::Auto, - "off" => SelectedObfuscation::Off, - "udp2tcp" => SelectedObfuscation::Udp2Tcp, - _ => unreachable!("Unhandled obfuscator mode"), - }; - Self::set_obfuscation_settings(&mut rpc, &settings).await?; + pub async fn handle(self) -> Result<()> { + match self { + Obfuscation::Get => { + let mut rpc = MullvadProxyClient::new().await?; + let obfuscation_settings = rpc.get_settings().await?.obfuscation_settings; + println!( + "Obfuscation mode: {}", + obfuscation_settings.selected_obfuscation + ); + println!("udp2tcp settings: {}", obfuscation_settings.udp2tcp); + Ok(()) } - Some(("udp2tcp", settings_matches)) => { - let port: String = settings_matches.value_of_t_or_exit("port"); - let mut rpc = new_rpc_client().await?; - let mut settings = Self::get_obfuscation_settings(&mut rpc).await?; - settings.udp2tcp.port = if port == "any" { - mullvad_types::relay_constraints::Constraint::Any - } else { - mullvad_types::relay_constraints::Constraint::Only( - port.parse::<u16>().expect("Invalid port number"), - ) - }; - Self::set_obfuscation_settings(&mut rpc, &settings).await?; - } - _ => unreachable!("unhandled command"), + Obfuscation::Set(subcmd) => Self::set(subcmd).await, } - Ok(()) } - async fn handle_get() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let obfuscation_settings = Self::get_obfuscation_settings(&mut rpc).await?; - println!( - "Obfuscation mode: {}", - obfuscation_settings.selected_obfuscation - ); - println!("udp2tcp settings: {}", obfuscation_settings.udp2tcp); - Ok(()) - } + async fn set(subcmd: SetCommands) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let current_settings = rpc.get_settings().await?.obfuscation_settings; - async fn get_obfuscation_settings( - rpc: &mut ManagementServiceClient, - ) -> Result<ObfuscationSettings> { - let settings = rpc.get_settings(()).await?.into_inner(); + match subcmd { + SetCommands::Mode { mode } => { + rpc.set_obfuscation_settings(ObfuscationSettings { + selected_obfuscation: mode, + ..current_settings + }) + .await?; + } + SetCommands::Udp2tcp { port } => { + rpc.set_obfuscation_settings(ObfuscationSettings { + udp2tcp: Udp2TcpObfuscationSettings { port }, + ..current_settings + }) + .await?; + } + } - let obfuscation_settings = ObfuscationSettings::try_from( - settings - .obfuscation_settings - .expect("No obfuscation settings"), - ) - .expect("failed to parse obfuscation settings"); - Ok(obfuscation_settings) - } + println!("Updated obfuscation settings"); - async fn set_obfuscation_settings( - rpc: &mut ManagementServiceClient, - settings: &ObfuscationSettings, - ) -> Result<()> { - let grpc_settings: grpc_types::ObfuscationSettings = settings.into(); - let _ = rpc.set_obfuscation_settings(grpc_settings).await?; Ok(()) } } - -fn create_obfuscation_set_subcommand() -> clap::App<'static> { - clap::App::new("set") - .about("Set obfuscation settings") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("mode").about("Set obfuscation mode").arg( - clap::Arg::new("mode") - .help( - "Specifies if obfuscation should be used with WireGuard connections. \ - And if so, what obfuscation protocol it should use.", - ) - .required(true) - .index(1) - .possible_values(["auto", "off", "udp2tcp"]), - ), - ) - .subcommand( - clap::App::new("udp2tcp") - .about("Specifies the config for the udp2tcp obfuscator") - .setting(clap::AppSettings::ArgRequiredElseHelp) - .arg( - clap::Arg::new("port") - .help("TCP port of remote endpoint. Either 'any' or a specific port") - .long("port") - .takes_value(true), - ), - ) -} - -fn create_obfuscation_get_subcommand() -> clap::App<'static> { - clap::App::new("get").about("Get current obfuscation settings") -} diff --git a/mullvad-cli/src/cmds/reconnect.rs b/mullvad-cli/src/cmds/reconnect.rs deleted file mode 100644 index 0a39d9f33d..0000000000 --- a/mullvad-cli/src/cmds/reconnect.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{format, new_rpc_client, state, Command, Error, Result}; -use futures::StreamExt; -use mullvad_types::states::TunnelState; - -pub struct Reconnect; - -#[mullvad_management_interface::async_trait] -impl Command for Reconnect { - fn name(&self) -> &'static str { - "reconnect" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Command the client to reconnect") - .arg( - clap::Arg::new("wait") - .long("wait") - .short('w') - .help("Wait until reconnected before exiting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - - let receiver_option = if matches.is_present("wait") { - Some(state::state_listen(rpc.clone())) - } else { - None - }; - - if rpc.reconnect_tunnel(()).await?.into_inner() { - if let Some(mut receiver) = receiver_option { - while let Some(state) = receiver.next().await { - let state = state?; - format::print_state(&state, false); - match state { - TunnelState::Connected { .. } => return Ok(()), - TunnelState::Error { .. } => return Err(Error::CommandFailed("reconnect")), - _ => {} - } - } - return Err(Error::StatusListenerFailed); - } - } - - Ok(()) - } -} diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 1497a7da41..37b86537e4 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -1,674 +1,181 @@ -use crate::{location, new_rpc_client, Command, Error, Result}; +use anyhow::{anyhow, Context, Result}; +use clap::Subcommand; use itertools::Itertools; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{ + location::Hostname, + relay_constraints::{ + Constraint, LocationConstraint, Match, OpenVpnConstraints, Ownership, Provider, Providers, + RelayConstraintsUpdate, RelaySettings, RelaySettingsUpdate, TransportPort, + WireguardConstraints, + }, + relay_list::{RelayEndpointData, RelayListCountry}, + ConnectionConfig, CustomTunnelEndpoint, +}; use std::{ - convert::TryFrom, - io::{self, BufRead}, + io::BufRead, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - str::FromStr, +}; +use talpid_types::net::{ + all_of_the_internet, openvpn, wireguard, Endpoint, IpVersion, TransportProtocol, TunnelType, }; -use mullvad_management_interface::{types, ManagementServiceClient}; -use mullvad_types::relay_constraints::{Constraint, RelaySettings}; -use talpid_types::net::all_of_the_internet; +use super::{relay_constraints::LocationArgs, BooleanOption}; -pub struct Relay; +#[derive(Subcommand, Debug)] +pub enum Relay { + /// Display the current relay constraints + Get, -#[mullvad_management_interface::async_trait] -impl Command for Relay { - fn name(&self) -> &'static str { - "relay" - } + /// Set relay constraints, such as location and port + #[clap(subcommand)] + Set(SetCommands), - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Manage relay and tunnel constraints") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("set") - .about( - "Set relay server selection parameters. Such as location and port/protocol", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("custom") - .about("Set a custom VPN relay") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("wireguard") - .arg( - clap::Arg::new("host") - .help("Hostname or IP") - .required(true), - ) - .arg( - clap::Arg::new("port") - .help("Remote network port") - .required(true), - ) - .arg( - clap::Arg::new("peer-pubkey") - .help("Base64 encoded peer public key") - .required(true), - ) - .arg( - clap::Arg::new("v4-gateway") - .help("IPv4 gateway address") - .required(true), - ) - .arg( - clap::Arg::new("addr") - .help("Local address of wireguard tunnel") - .required(true) - .multiple_values(true), - ) - .arg( - clap::Arg::new("v6-gateway") - .help("IPv6 gateway address") - .long("v6-gateway") - .takes_value(true), - ) - ) - .subcommand(clap::App::new("openvpn") - .arg( - clap::Arg::new("host") - .help("Hostname or IP") - .required(true), - ) - .arg( - clap::Arg::new("port") - .help("Remote network port") - .required(true), - ) - .arg( - clap::Arg::new("username") - .help("Username to be used with the OpenVpn relay") - .required(true), - ) - .arg( - clap::Arg::new("password") - .help("Password to be used with the OpenVpn relay") - .required(true), - ) - .arg( - clap::Arg::new("protocol") - .help("Transport protocol") - .long("protocol") - .default_value("udp") - .possible_values(["udp", "tcp"]), - ) - ) - ) - .subcommand( - location::get_subcommand() - .about("Set country or city to select relays from. Use the 'list' \ - command to show available alternatives.") - ) - .subcommand( - clap::App::new("hostname") - .about("Set the exact relay to use via its hostname. Shortcut for \ - 'location <country> <city> <hostname>'.") - .arg( - clap::Arg::new("hostname") - .help("The hostname") - .required(true), - ), - ) - .subcommand( - clap::App::new("provider") - .about("Set hosting provider(s) to select relays from. The 'list' \ - command shows the available relays and their providers.") - .arg( - clap::Arg::new("provider") - .help("The hosting provider(s) to use, or 'any' for no preference.") - .multiple_values(true) - .required(true) - ) - ) - .subcommand( - clap::App::new("ownership") - .about("Filters relays based on ownership. The 'list' \ - command shows the available relays and whether they're rented.") - .arg( - clap::Arg::new("ownership") - .help("Ownership preference, or 'any' for no preference.") - .possible_values(["any", "owned", "rented"]) - .required(true) - ) - ) - .subcommand( - clap::App::new("tunnel") - .about("Set tunnel protocol-specific constraints.") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::App::new("openvpn") - .about("Set OpenVPN-specific constraints") - .setting(clap::AppSettings::ArgRequiredElseHelp) - .arg( - clap::Arg::new("port") - .help("Port to use. Either 'any' or a specific port") - .long("port") - .takes_value(true), - ) - .arg( - clap::Arg::new("transport protocol") - .help("Transport protocol") - .long("protocol") - .possible_values(["any", "udp", "tcp"]) - .takes_value(true), - ) - ) - .subcommand( - clap::App::new("wireguard") - .about("Set WireGuard-specific constraints") - .setting(clap::AppSettings::ArgRequiredElseHelp) - .arg( - clap::Arg::new("port") - .help("Port to use. Either 'any' or a specific port") - .long("port") - .takes_value(true), - ) - .arg( - clap::Arg::new("ip version") - .long("ipv") - .possible_values(["any", "4", "6"]) - .takes_value(true), - ) - .arg( - clap::Arg::new("entry location") - .help("Entry endpoint to use. This can be 'any', 'none', or \ - any location that is valid with 'set location', \ - such as 'se got'.") - .long("entry-location") - .min_values(1) - .max_values(3), - ) - ) - ) - .subcommand(clap::App::new("tunnel-protocol") - .about("Set tunnel protocol") - .arg( - clap::Arg::new("tunnel protocol") - .required(true) - .index(1) - .possible_values(["any", "wireguard", "openvpn", ]), - ) - ), - ) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("list").about("List available countries and cities"), - ) - .subcommand( - clap::App::new("update") - .about("Update the list of available countries and cities"), - ) - } + /// List available relays + List, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - self.set(set_matches).await - } else if matches.subcommand_matches("get").is_some() { - self.get().await - } else if matches.subcommand_matches("list").is_some() { - self.list().await - } else if matches.subcommand_matches("update").is_some() { - self.update().await - } else { - unreachable!("No relay command given"); - } - } + /// Update the relay list + Update, } -impl Relay { - async fn update_constraints(&self, update: types::RelaySettingsUpdate) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.update_relay_settings(update) - .await - .map_err(|error| Error::RpcFailedExt("Failed to update relay settings", error))?; - println!("Relay constraints updated"); - Ok(()) - } - - async fn set(&self, matches: &clap::ArgMatches) -> Result<()> { - if let Some(custom_matches) = matches.subcommand_matches("custom") { - self.set_custom(custom_matches).await - } else if let Some(location_matches) = matches.subcommand_matches("location") { - self.set_location(location_matches).await - } else if let Some(relay_matches) = matches.subcommand_matches("hostname") { - self.set_hostname(relay_matches).await - } else if let Some(providers_matches) = matches.subcommand_matches("provider") { - self.set_providers(providers_matches).await - } else if let Some(ownership_matches) = matches.subcommand_matches("ownership") { - self.set_ownership(ownership_matches).await - } else if let Some(matches) = matches.subcommand_matches("tunnel") { - if let Some(tunnel_matches) = matches.subcommand_matches("openvpn") { - self.set_openvpn_constraints(tunnel_matches).await - } else if let Some(tunnel_matches) = matches.subcommand_matches("wireguard") { - self.set_wireguard_constraints(tunnel_matches).await - } else { - unreachable!("Invalid tunnel protocol"); - } - } else if let Some(tunnel_matches) = matches.subcommand_matches("tunnel-protocol") { - self.set_tunnel_protocol(tunnel_matches).await - } else { - unreachable!("No set relay command given"); - } - } - - async fn set_custom(&self, matches: &clap::ArgMatches) -> Result<()> { - let custom_endpoint = match matches.subcommand() { - Some(("openvpn", openvpn_matches)) => Self::read_custom_openvpn_relay(openvpn_matches), - Some(("wireguard", wg_matches)) => Self::read_custom_wireguard_relay(wg_matches), - _ => unreachable!("No set relay command given"), - }; - - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Custom(custom_endpoint)), - }) - .await - } - - fn read_custom_openvpn_relay(matches: &clap::ArgMatches) -> types::CustomRelaySettings { - let host = matches.value_of_t_or_exit("host"); - let port = matches.value_of_t_or_exit("port"); - let username = matches.value_of_t_or_exit("username"); - let password = matches.value_of_t_or_exit("password"); - let protocol: String = matches.value_of_t_or_exit("protocol"); - - let protocol = Self::validate_transport_protocol(&protocol); - - types::CustomRelaySettings { - host, - config: Some(types::ConnectionConfig { - config: Some(types::connection_config::Config::Openvpn( - types::connection_config::OpenvpnConfig { - address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port) - .to_string(), - protocol: protocol as i32, - username, - password, - }, - )), - }), - } - } - - fn read_custom_wireguard_relay(matches: &clap::ArgMatches) -> types::CustomRelaySettings { - use types::connection_config::wireguard_config; - - let host = matches.value_of_t_or_exit("host"); - let port = matches.value_of_t_or_exit("port"); - let addresses: Vec<IpAddr> = matches.values_of_t_or_exit("addr"); - let peer_key_str: String = matches.value_of_t_or_exit("peer-pubkey"); - let ipv4_gateway: Ipv4Addr = matches.value_of_t_or_exit("v4-gateway"); - let ipv6_gateway = match matches.value_of_t::<Ipv6Addr>("v6-gateway") { - Ok(gateway) => Some(gateway), - Err(e) => match e.kind { - clap::ErrorKind::ArgumentNotFound => None, - _ => e.exit(), - }, - }; - let mut private_key_str = String::new(); - println!("Reading private key from standard input"); - let _ = io::stdin().lock().read_line(&mut private_key_str); - if private_key_str.trim().is_empty() { - eprintln!("Expected to read private key from standard input"); - } - let private_key = Self::validate_wireguard_key(&private_key_str); - let peer_public_key = Self::validate_wireguard_key(&peer_key_str); - - types::CustomRelaySettings { - host, - config: Some(types::ConnectionConfig { - config: Some(types::connection_config::Config::Wireguard( - types::connection_config::WireguardConfig { - tunnel: Some(wireguard_config::TunnelConfig { - private_key: private_key.to_vec(), - addresses: addresses - .iter() - .map(|address| address.to_string()) - .collect(), - }), - peer: Some(wireguard_config::PeerConfig { - public_key: peer_public_key.to_vec(), - allowed_ips: all_of_the_internet() - .iter() - .map(|address| address.to_string()) - .collect(), - endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port) - .to_string(), - }), - ipv4_gateway: ipv4_gateway.to_string(), - ipv6_gateway: ipv6_gateway - .as_ref() - .map(|addr| addr.to_string()) - .unwrap_or_default(), - }, - )), - }), - } - } - - fn validate_wireguard_key(key_str: &str) -> [u8; 32] { - let key_bytes = base64::decode(key_str.trim()).unwrap_or_else(|e| { - eprintln!("Failed to decode wireguard key: {e}"); - std::process::exit(1); - }); - - let mut key = [0u8; 32]; - if key_bytes.len() != 32 { - eprintln!( - "Expected key length to be 32 bytes, got {}", - key_bytes.len() - ); - std::process::exit(1); - } - - key.copy_from_slice(&key_bytes); - key - } - - fn validate_transport_protocol(protocol: &str) -> types::TransportProtocol { - match protocol { - "udp" => types::TransportProtocol::Udp, - "tcp" => types::TransportProtocol::Tcp, - _ => clap::Error::raw( - clap::ErrorKind::ValueValidation, - "invalid transport protocol", - ) - .exit(), - } - } - - async fn set_hostname(&self, matches: &clap::ArgMatches) -> Result<()> { - let hostname = matches.value_of("hostname").unwrap(); - let countries = Self::get_filtered_relays().await?; - - let find_relay = || { - for country in countries { - for city in country.cities { - for relay in city.relays { - if relay.hostname.to_lowercase() == hostname.to_lowercase() { - return Some(types::RelayLocation { - country: country.code, - city: city.code, - hostname: relay.hostname, - }); - } - } - } - } - None - }; - - if let Some(location) = find_relay() { - println!( - "Setting location constraint to {} in {}, {}", - location.hostname, location.city, location.country - ); +#[derive(Subcommand, Debug, Clone)] +pub enum SetCommands { + /// Set country or city to select relays from. Use the 'list' + /// command to show available alternatives. + Location(LocationArgs), - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - location: Some(location), - ..Default::default() - }, - )), - }) - .await - } else { - clap::Error::raw(clap::ErrorKind::ValueValidation, "No matching server found").exit() - } - } - - async fn set_location(&self, matches: &clap::ArgMatches) -> Result<()> { - let location_constraint = location::get_constraint_from_args(matches); - let mut found = false; - - if !location_constraint.country.is_empty() { - // TODO: `mullvad_types::relay_constraints::LocationConstraint::matches(&relay)` - // could be used to guarantee consistency with the daemon. - let countries = Self::get_filtered_relays().await?; - for country in &countries { - if country.code != location_constraint.country { - continue; - } - - if location_constraint.city.is_empty() { - found = true; - break; - } + /// Set the location using only a hostname + Hostname { + /// A hostname, such as "se3-wireguard". + hostname: Hostname, + }, - for city in &country.cities { - if city.code != location_constraint.city { - continue; - } + /// Set hosting provider(s) to select relays from. The 'list' + /// command shows the available relays and their providers. + Provider { + #[arg(required(true), num_args = 1..)] + providers: Vec<Provider>, + }, - if location_constraint.hostname.is_empty() { - found = true; - break; - } + /// Filter relays based on ownership. The 'list' command + /// shows the available relays and whether they're rented. + Ownership { + /// Servers to select from: 'any', 'owned', or 'rented'. + ownership: Constraint<Ownership>, + }, - for relay in &city.relays { - if relay.hostname != location_constraint.hostname { - continue; - } - found = true; - break; - } + /// Set tunnel protocol specific constraints + #[clap(subcommand)] + Tunnel(SetTunnelCommands), - break; - } - break; - } + /// Set tunnel protocol to use: 'any', 'wireguard', or 'openvpn'. + TunnelProtocol { protocol: Constraint<TunnelType> }, - if !found { - eprintln!("Warning: No matching relay was found."); - } - } - - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - location: Some(location_constraint), - ..Default::default() - }, - )), - }) - .await - } - - async fn set_providers(&self, matches: &clap::ArgMatches) -> Result<()> { - let providers: Vec<String> = matches.values_of_t_or_exit("provider"); - let providers = if providers.get(0).map(String::as_str) == Some("any") { - vec![] - } else { - providers - }; - - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - providers: Some(types::ProviderUpdate { providers }), - ..Default::default() - }, - )), - }) - .await - } - - async fn set_ownership(&self, matches: &clap::ArgMatches) -> Result<()> { - let ownership = parse_ownership_constraint(matches.value_of("ownership").unwrap()); - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - ownership: Some(types::OwnershipUpdate { - ownership: ownership as i32, - }), - ..Default::default() - }, - )), - }) - .await - } + /// Set a custom VPN relay to use + #[clap(subcommand)] + Custom(SetCustomCommands), +} - async fn set_openvpn_constraints(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut openvpn_constraints = { - let mut rpc = new_rpc_client().await?; - self.get_openvpn_constraints(&mut rpc).await? - }; - openvpn_constraints.port = parse_transport_port(matches, &mut openvpn_constraints.port)?; +#[derive(Subcommand, Debug, Clone)] +pub enum SetTunnelCommands { + /// Set OpenVPN-specific constraints + #[clap(arg_required_else_help = true)] + Openvpn { + /// Port to use, or 'any' + #[arg(long, short = 'p', requires = "transport_protocol")] + port: Option<Constraint<u16>>, - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - openvpn_constraints: Some(openvpn_constraints), - ..Default::default() - }, - )), - }) - .await - } + /// Transport protocol to use, or 'any' + #[arg(long, short = 't')] + transport_protocol: Option<Constraint<TransportProtocol>>, + }, - async fn get_openvpn_constraints( - &self, - rpc: &mut ManagementServiceClient, - ) -> Result<types::OpenvpnConstraints> { - match rpc - .get_settings(()) - .await? - .into_inner() - .relay_settings - .unwrap() - .endpoint - .unwrap() - { - types::relay_settings::Endpoint::Normal(settings) => { - Ok(settings.openvpn_constraints.unwrap()) - } - types::relay_settings::Endpoint::Custom(_settings) => { - println!("Clearing custom tunnel constraints"); - Ok(types::OpenvpnConstraints::default()) - } - } - } + /// Set WireGuard-specific constraints + #[clap(arg_required_else_help = true)] + Wireguard { + /// Port to use, or 'any' + #[arg(long, short = 'p')] + port: Option<Constraint<u16>>, - async fn set_wireguard_constraints(&self, matches: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let relay_list = rpc - .get_relay_locations(()) - .await? - .into_inner() - .wireguard - .unwrap(); - let mut wireguard_constraints = self.get_wireguard_constraints(&mut rpc).await?; + /// IP protocol to use, or 'any' + #[arg(long, short = 'i')] + ip_version: Option<Constraint<IpVersion>>, - if let Some(port) = matches.value_of("port") { - wireguard_constraints.port = match parse_port_constraint(port)? { - Constraint::Any => 0, - Constraint::Only(specific_port) => { - let specific_port = u32::from(specific_port); + /// Whether to enable multihop. The location constraints are specified with + /// 'entry-location'. + #[arg(long, short = 'm')] + use_multihop: Option<BooleanOption>, - let is_valid_port = relay_list - .port_ranges - .iter() - .any(|range| range.first <= specific_port && specific_port <= range.last); - if !is_valid_port { - return Err(Error::CommandFailed("The specified port is invalid")); - } + #[clap(subcommand)] + entry_location: Option<EntryLocation>, + }, +} - specific_port - } - } - } +#[derive(Subcommand, Debug, Clone)] +pub enum EntryLocation { + /// Entry endpoint to use. This can be 'any' or any location that is valid with 'set location', + /// such as 'se got'. + EntryLocation(LocationArgs), +} - if let Some(ipv) = matches.value_of("ip version") { - wireguard_constraints.ip_version = - parse_ip_version_constraint(ipv).option().map(|protocol| { - types::IpVersionConstraint { - protocol: protocol as i32, - } - }); - } - if let Some(entry) = matches.values_of("entry location") { - wireguard_constraints.entry_location = parse_entry_location_constraint(entry); - let use_multihop = wireguard_constraints.entry_location.is_some(); - wireguard_constraints.use_multihop = use_multihop; - } +#[derive(Subcommand, Debug, Clone)] +pub enum SetCustomCommands { + /// Use a custom OpenVPN relay + #[clap(arg_required_else_help = true)] + Openvpn { + /// Hostname or IP + host: String, + /// Remote port + port: u16, + /// Username for authentication + username: String, + /// Password for authentication + password: String, + /// Transport protocol to use + #[arg(default_value_t = TransportProtocol::Udp)] + transport_protocol: TransportProtocol, + }, - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - wireguard_constraints: Some(wireguard_constraints), - ..Default::default() - }, - )), - }) - .await - } + /// Use a custom WireGuard relay + #[clap(arg_required_else_help = true)] + Wireguard { + /// Hostname or IP + host: String, + /// Remote port + port: u16, + /// Base64 encoded public key of remote peer + #[arg(value_parser = wireguard::PublicKey::from_base64)] + peer_pubkey: wireguard::PublicKey, + /// IP addresses of local tunnel interface + #[arg(required = true, num_args = 1..)] + tunnel_ip: Vec<IpAddr>, + /// IPv4 gateway address + #[arg(long)] + v4_gateway: Ipv4Addr, + /// IPv6 gateway address + #[arg(long)] + v6_gateway: Option<Ipv6Addr>, + }, +} - async fn get_wireguard_constraints( - &self, - rpc: &mut ManagementServiceClient, - ) -> Result<types::WireguardConstraints> { - match rpc - .get_settings(()) - .await? - .into_inner() - .relay_settings - .unwrap() - .endpoint - .unwrap() - { - types::relay_settings::Endpoint::Normal(settings) => { - Ok(settings.wireguard_constraints.unwrap()) - } - types::relay_settings::Endpoint::Custom(_settings) => { - println!("Clearing custom tunnel constraints"); - Ok(types::WireguardConstraints::default()) - } +impl Relay { + pub async fn handle(self) -> Result<()> { + match self { + Relay::Get => Self::get().await, + Relay::List => Self::list().await, + Relay::Update => Self::update().await, + Relay::Set(subcmd) => Self::set(subcmd).await, } } - async fn set_tunnel_protocol(&self, matches: &clap::ArgMatches) -> Result<()> { - let tunnel_type = match matches.value_of("tunnel protocol").unwrap() { - "wireguard" => Some(types::TunnelType::Wireguard), - "openvpn" => Some(types::TunnelType::Openvpn), - "any" => None, - _ => unreachable!(), - }; - self.update_constraints(types::RelaySettingsUpdate { - r#type: Some(types::relay_settings_update::Type::Normal( - types::NormalRelaySettingsUpdate { - tunnel_type: Some(types::TunnelTypeUpdate { - tunnel_type: tunnel_type.map(|tunnel_type| types::TunnelTypeConstraint { - tunnel_type: tunnel_type as i32, - }), - }), - ..Default::default() - }, - )), - }) - .await - } - - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let relay_settings = rpc - .get_settings(()) - .await? - .into_inner() - .relay_settings - .unwrap(); - - println!( - "Current constraints: {}", - RelaySettings::try_from(relay_settings).unwrap() - ); - + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let relay_settings = rpc.get_settings().await?.relay_settings; + println!("Current constraints: {relay_settings}"); Ok(()) } - async fn list(&self) -> Result<()> { + async fn list() -> Result<()> { let mut countries = Self::get_filtered_relays().await?; countries.sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); for mut country in countries { @@ -684,9 +191,9 @@ impl Relay { city.name, city.code, city.latitude, city.longitude ); for relay in &city.relays { - let support_msg = match relay.endpoint_type { - i if i == i32::from(types::relay::RelayType::Openvpn) => "OpenVPN", - i if i == i32::from(types::relay::RelayType::Wireguard) => "WireGuard", + let support_msg = match relay.endpoint_data { + RelayEndpointData::Openvpn => "OpenVPN", + RelayEndpointData::Wireguard(_) => "WireGuard", _ => unreachable!("Bug in relay filtering earlier on"), }; let ownership = if relay.owned { @@ -694,9 +201,9 @@ impl Relay { } else { "rented" }; - let mut addresses = vec![&relay.ipv4_addr_in]; - if !relay.ipv6_addr_in.is_empty() { - addresses.push(&relay.ipv6_addr_in); + let mut addresses: Vec<IpAddr> = vec![relay.ipv4_addr_in.into()]; + if let Some(ipv6_addr) = relay.ipv6_addr_in { + addresses.push(ipv6_addr.into()); } println!( "\t\t{} ({}) - {}, hosted by {} ({ownership})", @@ -712,21 +219,20 @@ impl Relay { Ok(()) } - async fn update(&self) -> Result<()> { - new_rpc_client().await?.update_relay_locations(()).await?; + async fn update() -> Result<()> { + MullvadProxyClient::new() + .await? + .update_relay_locations() + .await?; println!("Updating relay list in the background..."); Ok(()) } - async fn get_filtered_relays() -> Result<Vec<types::RelayListCountry>> { - let mut rpc = new_rpc_client().await?; - let relay_list = rpc - .get_relay_locations(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain relay locations", error))? - .into_inner(); + async fn get_filtered_relays() -> Result<Vec<RelayListCountry>> { + let mut rpc = MullvadProxyClient::new().await?; + let relay_list = rpc.get_relay_locations().await?; - let mut countries = Vec::new(); + let mut countries = vec![]; for mut country in relay_list.countries { country.cities = country @@ -734,8 +240,7 @@ impl Relay { .into_iter() .filter_map(|mut city| { city.relays.retain(|relay| { - relay.active - && relay.endpoint_type != (types::relay::RelayType::Bridge as i32) + relay.active && relay.endpoint_data != RelayEndpointData::Bridge }); if !city.relays.is_empty() { Some(city) @@ -751,108 +256,334 @@ impl Relay { Ok(countries) } -} -fn parse_port_constraint(raw_port: &str) -> Result<Constraint<u16>> { - match raw_port.to_lowercase().as_str() { - "any" => Ok(Constraint::Any), - port => Ok(Constraint::Only(u16::from_str(port).map_err(|_| { - Error::InvalidCommand("Invalid port. Must be \"any\" or 0-65535.") - })?)), + async fn update_constraints(update: RelaySettingsUpdate) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.update_relay_settings(update).await?; + println!("Relay constraints updated"); + Ok(()) } -} -fn parse_protocol(raw_protocol: &str) -> Constraint<types::TransportProtocol> { - match raw_protocol { - "any" => Constraint::Any, - "udp" => Constraint::Only(types::TransportProtocol::Udp), - "tcp" => Constraint::Only(types::TransportProtocol::Tcp), - _ => unreachable!(), + async fn set(subcmd: SetCommands) -> Result<()> { + match subcmd { + SetCommands::Custom(subcmd) => Self::set_custom(subcmd).await, + SetCommands::Location(location) => Self::set_location(location).await, + SetCommands::Hostname { hostname } => Self::set_hostname(hostname).await, + SetCommands::Provider { providers } => Self::set_providers(providers).await, + SetCommands::Ownership { ownership } => Self::set_ownership(ownership).await, + SetCommands::Tunnel(subcmd) => Self::set_tunnel(subcmd).await, + SetCommands::TunnelProtocol { protocol } => Self::set_tunnel_protocol(protocol).await, + } } -} -fn parse_ip_version_constraint(raw_protocol: &str) -> Constraint<types::IpVersion> { - match raw_protocol { - "any" => Constraint::Any, - "4" => Constraint::Only(types::IpVersion::V4), - "6" => Constraint::Only(types::IpVersion::V6), - _ => unreachable!(), + async fn set_tunnel(subcmd: SetTunnelCommands) -> Result<()> { + match subcmd { + SetTunnelCommands::Openvpn { + port, + transport_protocol, + } => Self::set_openvpn_constraints(port, transport_protocol).await, + SetTunnelCommands::Wireguard { + port, + ip_version, + use_multihop, + entry_location, + } => { + Self::set_wireguard_constraints(port, ip_version, use_multihop, entry_location) + .await + } + } + } + + async fn set_custom(subcmd: SetCustomCommands) -> Result<()> { + let custom_endpoint = match subcmd { + SetCustomCommands::Openvpn { + host, + port, + username, + password, + transport_protocol, + } => { + Self::read_custom_openvpn_relay(host, port, username, password, transport_protocol) + } + SetCustomCommands::Wireguard { + host, + port, + peer_pubkey, + tunnel_ip, + v4_gateway, + v6_gateway, + } => { + Self::read_custom_wireguard_relay( + host, + port, + peer_pubkey, + tunnel_ip, + v4_gateway, + v6_gateway, + ) + .await? + } + }; + Self::update_constraints(RelaySettingsUpdate::CustomTunnelEndpoint(custom_endpoint)).await + } + + fn read_custom_openvpn_relay( + host: String, + port: u16, + username: String, + password: String, + protocol: TransportProtocol, + ) -> CustomTunnelEndpoint { + CustomTunnelEndpoint { + host, + config: ConnectionConfig::OpenVpn(openvpn::ConnectionConfig { + endpoint: Endpoint::from_socket_address( + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), + protocol, + ), + username, + password, + }), + } } -} -fn parse_entry_location_constraint<'a, T: Iterator<Item = &'a str>>( - mut location: T, -) -> Option<types::RelayLocation> { - let country = location.next().unwrap(); + async fn read_custom_wireguard_relay( + host: String, + port: u16, + peer_pubkey: wireguard::PublicKey, + tunnel_ip: Vec<IpAddr>, + ipv4_gateway: Ipv4Addr, + ipv6_gateway: Option<Ipv6Addr>, + ) -> Result<CustomTunnelEndpoint> { + println!("Reading private key from standard input"); + + let private_key_str = tokio::task::spawn_blocking(|| { + let mut private_key_str = String::new(); + let _ = std::io::stdin().lock().read_line(&mut private_key_str); + if private_key_str.trim().is_empty() { + eprintln!("Expected to read private key from standard input"); + } + private_key_str + }) + .await + .unwrap(); + + let private_key = + wireguard::PrivateKey::from_base64(&private_key_str).context("Invalid private key")?; - if country == "none" { - return None; + Ok(CustomTunnelEndpoint { + host, + config: ConnectionConfig::Wireguard(wireguard::ConnectionConfig { + tunnel: wireguard::TunnelConfig { + private_key, + addresses: tunnel_ip, + }, + peer: wireguard::PeerConfig { + public_key: peer_pubkey, + allowed_ips: all_of_the_internet(), + endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), + psk: None, + }, + exit_peer: None, + ipv4_gateway, + ipv6_gateway, + // NOTE: Ignored in gRPC + #[cfg(target_os = "linux")] + fwmark: None, + }), + }) } - Some(location::get_constraint( - country, - location.next(), - location.next(), - )) -} + async fn set_hostname(hostname: String) -> Result<()> { + let countries = Self::get_filtered_relays().await?; -fn parse_transport_port( - matches: &clap::ArgMatches, - current_constraint: &mut Option<types::TransportPort>, -) -> Result<Option<types::TransportPort>> { - let protocol = match matches.value_of("transport protocol") { - Some(protocol) => parse_protocol(protocol), - None => { - if let Some(ref transport_port) = current_constraint { - Constraint::Only( - types::TransportProtocol::from_i32(transport_port.protocol).unwrap(), - ) - } else { - Constraint::Any + let find_relay = || { + for country in countries { + for city in country.cities { + for relay in city.relays { + if relay.hostname.to_lowercase() == hostname.to_lowercase() { + return Some(LocationConstraint::Hostname( + country.code, + city.code, + relay.hostname, + )); + } + } + } + } + None + }; + + let location = find_relay().ok_or(anyhow!("Hostname not found"))?; + + println!("Setting location constraint to {location}"); + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + location: Some(Constraint::Only(location)), + ..Default::default() + })) + .await + } + + async fn set_location(location_constraint: LocationArgs) -> Result<()> { + let location_constraint = Constraint::from(location_constraint); + match &location_constraint { + Constraint::Any => (), + Constraint::Only(constraint) => { + let countries = Self::get_filtered_relays().await?; + + let found = countries + .into_iter() + .flat_map(|country| country.cities) + .flat_map(|city| city.relays) + .any(|relay| constraint.matches(&relay)); + + if !found { + eprintln!("Warning: No matching relay was found."); + } } } - }; - let mut port = match matches.value_of("port") { - Some(port) => parse_port_constraint(port)?, - None => { - if let Some(ref transport_port) = current_constraint { - if transport_port.port != 0 { - Constraint::Only(transport_port.port as u16) - } else { - Constraint::Any + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + location: Some(location_constraint), + ..Default::default() + })) + .await + } + + async fn set_providers(providers: Vec<String>) -> Result<()> { + let providers = if providers[0].eq_ignore_ascii_case("any") { + Constraint::Any + } else { + Constraint::Only(Providers::new(providers.into_iter()).unwrap()) + }; + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + providers: Some(providers), + ..Default::default() + })) + .await + } + + async fn set_ownership(ownership: Constraint<Ownership>) -> Result<()> { + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + ownership: Some(ownership), + ..Default::default() + })) + .await + } + + async fn set_openvpn_constraints( + port: Option<Constraint<u16>>, + protocol: Option<Constraint<TransportProtocol>>, + ) -> Result<()> { + let mut openvpn_constraints = { + let mut rpc = MullvadProxyClient::new().await?; + Self::get_openvpn_constraints(&mut rpc).await? + }; + openvpn_constraints.port = + parse_transport_port(port, protocol, &mut openvpn_constraints.port); + + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + openvpn_constraints: Some(openvpn_constraints), + ..Default::default() + })) + .await + } + + async fn get_openvpn_constraints(rpc: &mut MullvadProxyClient) -> Result<OpenVpnConstraints> { + match rpc.get_settings().await?.relay_settings { + RelaySettings::Normal(settings) => Ok(settings.openvpn_constraints), + RelaySettings::CustomTunnelEndpoint(_settings) => { + println!("Clearing custom tunnel constraints"); + Ok(OpenVpnConstraints::default()) + } + } + } + + async fn set_wireguard_constraints( + port: Option<Constraint<u16>>, + ip_version: Option<Constraint<IpVersion>>, + use_multihop: Option<BooleanOption>, + entry_location: Option<EntryLocation>, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let wireguard = rpc.get_relay_locations().await?.wireguard; + let mut wireguard_constraints = Self::get_wireguard_constraints(&mut rpc).await?; + + if let Some(port) = port { + wireguard_constraints.port = match port { + Constraint::Any => Constraint::Any, + Constraint::Only(specific_port) => { + let is_valid_port = wireguard + .port_ranges + .into_iter() + .any(|(first, last)| first <= specific_port && specific_port <= last); + if !is_valid_port { + return Err(anyhow!("The specified port is invalid")); + } + Constraint::Only(specific_port) } - } else { - Constraint::Any } } - }; - if port.is_only() && protocol.is_any() && !matches.is_present("port") { - // Reset the port if the transport protocol is set to any. - println!("The port constraint was set to 'any'"); - port = Constraint::Any; + + if let Some(ipv) = ip_version { + wireguard_constraints.ip_version = ipv; + } + if let Some(use_multihop) = use_multihop { + wireguard_constraints.use_multihop = *use_multihop; + } + if let Some(EntryLocation::EntryLocation(entry)) = entry_location { + wireguard_constraints.entry_location = Constraint::from(entry); + } + + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + wireguard_constraints: Some(wireguard_constraints), + ..Default::default() + })) + .await } - match (port, protocol) { - (Constraint::Any, Constraint::Any) => Ok(None), - (Constraint::Any, Constraint::Only(protocol)) => Ok(Some(types::TransportPort { - protocol: protocol as i32, - // If no port was specified, set it to "any" - ..types::TransportPort::default() - })), - (Constraint::Only(port), Constraint::Only(protocol)) => Ok(Some(types::TransportPort { - protocol: protocol as i32, - port: u32::from(port), - })), - (Constraint::Only(_), Constraint::Any) => Err(Error::InvalidCommand( - "a transport protocol must be given to select a specific port", - )), + + async fn get_wireguard_constraints( + rpc: &mut MullvadProxyClient, + ) -> Result<WireguardConstraints> { + match rpc.get_settings().await?.relay_settings { + RelaySettings::Normal(settings) => Ok(settings.wireguard_constraints), + RelaySettings::CustomTunnelEndpoint(_settings) => { + println!("Clearing custom tunnel constraints"); + Ok(WireguardConstraints::default()) + } + } + } + + async fn set_tunnel_protocol(protocol: Constraint<TunnelType>) -> Result<()> { + Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + tunnel_protocol: Some(protocol), + ..Default::default() + })) + .await } } -pub fn parse_ownership_constraint(constraint: &str) -> types::Ownership { - match constraint { - "any" => types::Ownership::Any, - "owned" => types::Ownership::MullvadOwned, - "rented" => types::Ownership::Rented, - _ => unreachable!(), +fn parse_transport_port( + port: Option<Constraint<u16>>, + protocol: Option<Constraint<TransportProtocol>>, + current_constraint: &mut Constraint<TransportPort>, +) -> Constraint<TransportPort> { + let port = match port { + Some(port) => port, + None => current_constraint + .map(|p| p.port) + .unwrap_or(Constraint::Any), + }; + let protocol = match protocol { + Some(protocol) => protocol, + None => current_constraint.map(|p| p.protocol), + }; + match (port, protocol) { + (port, Constraint::Any) => { + if port.is_only() { + println!("The port constraint was set to 'any'"); + } + Constraint::Any + } + (port, Constraint::Only(protocol)) => Constraint::Only(TransportPort { protocol, port }), } } diff --git a/mullvad-cli/src/cmds/relay_constraints.rs b/mullvad-cli/src/cmds/relay_constraints.rs new file mode 100644 index 0000000000..1fc5073a4d --- /dev/null +++ b/mullvad-cli/src/cmds/relay_constraints.rs @@ -0,0 +1,34 @@ +use clap::Args; +use mullvad_types::{ + location::{CityCode, CountryCode, Hostname}, + relay_constraints::{Constraint, LocationConstraint}, +}; + +#[derive(Args, Debug, Clone)] +pub struct LocationArgs { + /// A two-letter country code, or 'any'. + pub country: CountryCode, + /// A three-letter city code. + pub city: Option<CityCode>, + /// A host name, such as "se-got-wg-101". + pub hostname: Option<Hostname>, +} + +impl From<LocationArgs> for Constraint<LocationConstraint> { + fn from(value: LocationArgs) -> Self { + if value.country.eq_ignore_ascii_case("any") { + return Constraint::Any; + } + + match (value.country, value.city, value.hostname) { + (country, None, None) => Constraint::Only(LocationConstraint::Country(country)), + (country, Some(city), None) => { + Constraint::Only(LocationConstraint::City(country, city)) + } + (country, Some(city), Some(hostname)) => { + Constraint::Only(LocationConstraint::Hostname(country, city, hostname)) + } + _ => unreachable!("invalid location arguments"), + } + } +} diff --git a/mullvad-cli/src/cmds/reset.rs b/mullvad-cli/src/cmds/reset.rs index d3e3ec3e62..a8c275a042 100644 --- a/mullvad-cli/src/cmds/reset.rs +++ b/mullvad-cli/src/cmds/reset.rs @@ -1,44 +1,32 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::Result; +use mullvad_management_interface::MullvadProxyClient; use std::io::stdin; -pub struct Reset; -#[mullvad_management_interface::async_trait] -impl Command for Reset { - fn name(&self) -> &'static str { - "factory-reset" +pub async fn handle() -> Result<()> { + if receive_confirmation().await { + let mut rpc = MullvadProxyClient::new().await?; + rpc.factory_reset().await?; + #[cfg(target_os = "linux")] + println!("If you're running systemd, to remove all logs, you must use journalctl"); } + Ok(()) +} - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()).about("Reset settings, caches and logs") - } +async fn receive_confirmation() -> bool { + println!("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service? [Yes/No (default)]"); - async fn run(&self, _: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - if Self::receive_confirmation() { - rpc.factory_reset(()) - .await - .map_err(|error| Error::RpcFailedExt("FAILED TO PERFORM FACTORY RESET", error))?; - #[cfg(target_os = "linux")] - println!("If you're running systemd, to remove all logs, you must use journalctl"); + tokio::task::spawn_blocking(|| loop { + let mut buf = String::new(); + if let Err(e) = stdin().read_line(&mut buf) { + eprintln!("Couldn't read from STDIN: {e}"); + return false; } - Ok(()) - } -} - -impl Reset { - fn receive_confirmation() -> bool { - println!("Are you sure you want to disconnect, log out, delete all settings, logs and cache files for the Mullvad VPN system service? [Yes/No (default)]"); - loop { - let mut buf = String::new(); - if let Err(e) = stdin().read_line(&mut buf) { - eprintln!("Couldn't read from STDIN: {e}"); - return false; - } - match buf.trim() { - "Yes" => return true, - "No" | "no" | "" => return false, - _ => println!("Unexpected response. Please enter \"Yes\" or \"No\""), - } + match buf.trim() { + "Yes" => return true, + "No" | "no" | "" => return false, + _ => eprintln!("Unexpected response. Please enter \"Yes\" or \"No\""), } - } + }) + .await + .unwrap() } diff --git a/mullvad-cli/src/cmds/split_tunnel/linux.rs b/mullvad-cli/src/cmds/split_tunnel/linux.rs index 8b235f1027..5a66d899ab 100644 --- a/mullvad-cli/src/cmds/split_tunnel/linux.rs +++ b/mullvad-cli/src/cmds/split_tunnel/linux.rs @@ -1,82 +1,61 @@ -use crate::{new_rpc_client, Command, Result}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct SplitTunnel; - -#[mullvad_management_interface::async_trait] -impl Command for SplitTunnel { - fn name(&self) -> &'static str { - "split-tunnel" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about( - "Manage split tunneling. To launch applications outside \ - the tunnel, use the program 'mullvad-exclude' instead of this command.", - ) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_pid_subcommand()) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("pid", pid_matches)) => Self::handle_pid_cmd(pid_matches).await, - _ => unreachable!("unhandled command"), - } - } -} - -fn create_pid_subcommand() -> clap::App<'static> { - clap::App::new("pid") - .about("Manage processes to exclude from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("add").arg(clap::Arg::new("pid").required(true))) - .subcommand(clap::App::new("delete").arg(clap::Arg::new("pid").required(true))) - .subcommand(clap::App::new("clear")) - .subcommand(clap::App::new("list")) +/// Manage split tunneling. To launch applications outside the tunnel, use the program +/// 'mullvad-exclude' instead of this command +#[derive(Subcommand, Debug)] +pub enum SplitTunnel { + /// List all processes that are excluded from the tunnel + List, + /// Add a PID to exclude from the tunnel + Add { pid: i32 }, + /// Stop excluding a PID from the tunnel + Delete { pid: i32 }, + /// Stop excluding all processes from the tunnel + Clear, } impl SplitTunnel { - async fn handle_pid_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("add", matches)) => { - let pid: i32 = matches.value_of_t_or_exit("pid"); - new_rpc_client() + pub async fn handle(self) -> Result<()> { + match self { + SplitTunnel::List => { + let pids = MullvadProxyClient::new() .await? - .add_split_tunnel_process(pid) + .get_split_tunnel_processes() .await?; + + println!("Excluded PIDs:"); + for pid in &pids { + println!("{pid}"); + } + Ok(()) } - Some(("delete", matches)) => { - let pid: i32 = matches.value_of_t_or_exit("pid"); - new_rpc_client() + SplitTunnel::Add { pid } => { + MullvadProxyClient::new() .await? - .remove_split_tunnel_process(pid) + .add_split_tunnel_process(pid) .await?; + println!("Excluding process"); Ok(()) } - Some(("clear", _)) => { - new_rpc_client() + SplitTunnel::Delete { pid } => { + MullvadProxyClient::new() .await? - .clear_split_tunnel_processes(()) + .remove_split_tunnel_process(pid) .await?; + println!("Stopped excluding process"); Ok(()) } - Some(("list", _)) => { - let mut pids_stream = new_rpc_client() - .await? - .get_split_tunnel_processes(()) + SplitTunnel::Clear => { + MullvadProxyClient::new() .await? - .into_inner(); - println!("Excluded PIDs:"); - - while let Some(pid) = pids_stream.message().await? { - println!(" {pid}"); - } - + .clear_split_tunnel_processes() + .await?; + println!("Stopped excluding all processes"); Ok(()) } - _ => unreachable!("unhandled command"), } } } diff --git a/mullvad-cli/src/cmds/split_tunnel/windows.rs b/mullvad-cli/src/cmds/split_tunnel/windows.rs index a133ab9682..f17a3382f3 100644 --- a/mullvad-cli/src/cmds/split_tunnel/windows.rs +++ b/mullvad-cli/src/cmds/split_tunnel/windows.rs @@ -1,157 +1,110 @@ -use std::{ffi::OsStr, path::Path}; +use anyhow::Result; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; -use crate::{new_rpc_client, Command, Result}; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; -pub struct SplitTunnel; +use super::super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for SplitTunnel { - fn name(&self) -> &'static str { - "split-tunnel" - } +/// Set options for applications to exclude from the tunnel. +#[derive(Subcommand, Debug)] +pub enum SplitTunnel { + /// Display the split tunnel status and apps + Get { + /// List processes that are currently being excluded, as well as whether they are + /// excluded because of their executable paths or because they're subprocesses of + /// such processes + #[arg(long)] + list_processes: bool, + }, - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Set options for applications to exclude from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_app_subcommand()) - .subcommand( - clap::App::new("set") - .about("Enable or disable split tunnel") - .arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off"]), - ), - ) - .subcommand(clap::App::new("get").about("Display the split tunnel status")) - .subcommand(create_pid_subcommand()) - } + /// Enable or disable split tunnel + Set { policy: BooleanOption }, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("app", matches)) => Self::handle_app_subcommand(matches).await, - Some(("pid", matches)) => Self::handle_pid_subcommand(matches).await, - Some(("get", _)) => self.get().await, - Some(("set", matches)) => { - let enabled = matches.value_of("policy").expect("missing policy"); - self.set(enabled == "on").await - } - _ => { - unreachable!("unhandled command"); - } - } - } -} - -fn create_app_subcommand() -> clap::App<'static> { - clap::App::new("app") - .about("Manage applications to exclude from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("list")) - .subcommand(clap::App::new("add").arg(clap::Arg::new("path").required(true))) - .subcommand(clap::App::new("remove").arg(clap::Arg::new("path").required(true))) - .subcommand(clap::App::new("clear")) + /// Manage applications to exclude from the tunnel + #[clap(subcommand)] + App(App), } -fn create_pid_subcommand() -> clap::App<'static> { - clap::App::new("pid") - .about("Manages processes (PIDs) excluded from the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("list") - .about("List processes that are currently being excluded, i.e. their PIDs, as well as whether \ - they are excluded because of their executable paths or because they're subprocesses of \ - such processes")) +#[derive(Subcommand, Debug)] +pub enum App { + Add { path: PathBuf }, + Remove { path: PathBuf }, + Clear, } impl SplitTunnel { - async fn handle_app_subcommand(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("list", _)) => { - let paths = new_rpc_client() - .await? - .get_settings(()) - .await? - .into_inner() - .split_tunnel - .unwrap() - .apps; + pub async fn handle(self) -> Result<()> { + match self { + SplitTunnel::Get { list_processes } => { + let mut rpc = MullvadProxyClient::new().await?; + let settings = rpc.get_settings().await?.split_tunnel; + + let enable_exclusions = BooleanOption::from(settings.enable_exclusions); + + println!("Split tunneling state: {enable_exclusions}"); println!("Excluded applications:"); - for path in &paths { - println!(" {}", path); + for path in &settings.apps { + println!("{}", path.display()); + } + + if list_processes { + let processes = rpc.get_excluded_processes().await?; + for process in &processes { + let subproc = if process.inherited { "subprocess" } else { "" }; + println!( + "{:<7}{subproc:<12}{}", + process.pid, + Path::new(&process.image) + .file_name() + .unwrap_or(OsStr::new("unknown")) + .to_string_lossy() + ); + } } Ok(()) } - Some(("add", matches)) => { - let path: String = matches.value_of_t_or_exit("path"); - new_rpc_client().await?.add_split_tunnel_app(path).await?; + SplitTunnel::Set { policy } => { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_split_tunnel_state(*policy).await?; + println!("Split tunnel policy: {policy}"); Ok(()) } - Some(("remove", matches)) => { - let path: String = matches.value_of_t_or_exit("path"); - new_rpc_client() + SplitTunnel::App(subcmd) => Self::app(subcmd).await, + } + } + + async fn app(subcmd: App) -> Result<()> { + match subcmd { + App::Add { path } => { + MullvadProxyClient::new() .await? - .remove_split_tunnel_app(path) + .add_split_tunnel_app(path) .await?; + println!("Added path to excluded apps list"); Ok(()) } - Some(("clear", _)) => { - new_rpc_client().await?.clear_split_tunnel_apps(()).await?; + App::Remove { path } => { + MullvadProxyClient::new() + .await? + .remove_split_tunnel_app(path) + .await?; + println!("Stopped excluding app from tunnel"); Ok(()) } - _ => unreachable!("unhandled subcommand"), - } - } - - async fn handle_pid_subcommand(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("list", _)) => { - let processes = new_rpc_client() - .await? - .get_excluded_processes(()) + App::Clear => { + MullvadProxyClient::new() .await? - .into_inner(); - - for process in &processes.processes { - let subproc = if process.inherited { "subprocess" } else { "" }; - println!( - "{:<7}{subproc:<12}{}", - process.pid, - Path::new(&process.image) - .file_name() - .unwrap_or(OsStr::new("unknown")) - .to_string_lossy() - ); - } - + .clear_split_tunnel_apps() + .await?; + println!("Stopped excluding all apps"); Ok(()) } - _ => unreachable!("unhandled subcommand"), } } - - async fn set(&self, enabled: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_split_tunnel_state(enabled).await?; - println!("Changed split tunnel setting"); - Ok(()) - } - - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let enabled = rpc - .get_settings(()) - .await? - .into_inner() - .split_tunnel - .unwrap() - .enable_exclusions; - println!( - "Split tunnel status: {}", - if enabled { "on" } else { "off" } - ); - Ok(()) - } } diff --git a/mullvad-cli/src/cmds/status.rs b/mullvad-cli/src/cmds/status.rs index 828d17af0e..8ddd195333 100644 --- a/mullvad-cli/src/cmds/status.rs +++ b/mullvad-cli/src/cmds/status.rs @@ -1,127 +1,113 @@ -use crate::{format, new_rpc_client, Command, Error, Result}; -use mullvad_management_interface::{ - types::daemon_event::Event as EventType, ManagementServiceClient, -}; -use mullvad_types::{location::GeoIpLocation, states::TunnelState}; +use anyhow::Result; +use clap::{Args, Subcommand}; +use futures::StreamExt; +use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; +use mullvad_types::states::TunnelState; -pub struct Status; +use crate::format; -#[mullvad_management_interface::async_trait] -impl Command for Status { - fn name(&self) -> &'static str { - "status" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("View the state of the VPN tunnel") - .arg( - clap::Arg::new("verbose") - .short('v') - .help("Enables verbose output"), - ) - .arg( - clap::Arg::new("location") - .long("location") - .short('l') - .help("Prints the current location and IP. Based on GeoIP lookups"), - ) - .arg( - clap::Arg::new("debug") - .long("debug") - .global(true) - .help("Enables debug output"), - ) - .subcommand(clap::App::new("listen").about("Listen for VPN tunnel state changes")) - } - - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - let debug = matches.is_present("debug"); - let verbose = matches.is_present("verbose"); - let show_full_location = matches.is_present("location"); - - let mut rpc = new_rpc_client().await?; - let state = rpc.get_tunnel_state(()).await?.into_inner(); - - if debug { - println!("Tunnel state: {state:#?}"); - } else { - let state = TunnelState::try_from(state).expect("invalid tunnel state"); - format::print_state(&state, verbose); - } +#[derive(Subcommand, Debug, PartialEq)] +pub enum Status { + /// Listen for tunnel state changes + Listen, +} - if show_full_location { - print_location(&mut rpc).await?; - } +#[derive(Args, Debug)] +pub struct StatusArgs { + /// Enable verbose output + #[arg(long, short = 'v')] + verbose: bool, - if matches.subcommand_matches("listen").is_some() { - let mut events = rpc.events_listen(()).await?.into_inner(); + /// Print the current location and IP, based on GeoIP lookups + #[arg(long, short = 'l')] + location: bool, - while let Some(event) = events.message().await? { - match event.event.unwrap() { - EventType::TunnelState(new_state) => { - let new_state = - TunnelState::try_from(new_state).expect("invalid tunnel state"); + /// Enable debug output + #[arg(long, short = 'd')] + debug: bool, +} - if debug { - println!("New tunnel state: {new_state:#?}"); - } else { - format::print_state(&new_state, verbose); - } +impl Status { + pub async fn listen(mut rpc: MullvadProxyClient, args: StatusArgs) -> Result<()> { + while let Some(event) = rpc.events_listen().await?.next().await { + match event? { + DaemonEvent::TunnelState(new_state) => { + if args.debug { + println!("New tunnel state: {new_state:#?}"); + } else { + format::print_state(&new_state, args.verbose); + } - match new_state { - TunnelState::Connected { .. } | TunnelState::Disconnected => { - if show_full_location { - print_location(&mut rpc).await?; - } + match new_state { + TunnelState::Connected { .. } | TunnelState::Disconnected => { + if args.location { + print_location(&mut rpc).await?; } - _ => {} } + _ => {} } - EventType::Settings(settings) => { - if debug { - println!("New settings: {settings:#?}"); - } + } + DaemonEvent::Settings(settings) => { + if args.debug { + println!("New settings: {settings:#?}"); } - EventType::RelayList(relay_list) => { - if debug { - println!("New relay list: {relay_list:#?}"); - } + } + DaemonEvent::RelayList(relay_list) => { + if args.debug { + println!("New relay list: {relay_list:#?}"); } - EventType::VersionInfo(app_version_info) => { - if debug { - println!("New app version info: {app_version_info:#?}"); - } + } + DaemonEvent::AppVersionInfo(app_version_info) => { + if args.debug { + println!("New app version info: {app_version_info:#?}"); } - EventType::Device(device) => { - if debug { - println!("Device event: {device:#?}"); - } + } + DaemonEvent::Device(device) => { + if args.debug { + println!("Device event: {device:#?}"); } - EventType::RemoveDevice(device) => { - if debug { - println!("Remove device event: {device:#?}"); - } + } + DaemonEvent::RemoveDevice(device) => { + if args.debug { + println!("Remove device event: {device:#?}"); } } } } - Ok(()) } } -async fn print_location(rpc: &mut ManagementServiceClient) -> Result<()> { - let location = match rpc.get_current_location(()).await { - Ok(response) => GeoIpLocation::try_from(response.into_inner()).expect("invalid geoip data"), - Err(status) => { - if status.code() == mullvad_management_interface::Code::NotFound { +pub async fn handle(cmd: Option<Status>, args: StatusArgs) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let state = rpc.get_tunnel_state().await?; + + if args.debug { + println!("Tunnel state: {state:#?}"); + } else { + format::print_state(&state, args.verbose); + } + + if args.location { + print_location(&mut rpc).await?; + } + + if cmd == Some(Status::Listen) { + Status::listen(rpc, args).await?; + } + Ok(()) +} + +async fn print_location(rpc: &mut MullvadProxyClient) -> Result<()> { + let location = match rpc.get_current_location().await { + Ok(location) => location, + Err(error) => match &error { + mullvad_management_interface::Error::NoLocationData => { println!("Location data unavailable"); return Ok(()); - } else { - return Err(Error::RpcFailed(status)); } - } + _ => return Err(error.into()), + }, }; if let Some(ipv4) = location.ipv4 { println!("IPv4: {ipv4}"); diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 1d2fd1a7bb..120fad327d 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -1,436 +1,204 @@ -use crate::{new_rpc_client, Command, Error, Result}; -use mullvad_management_interface::types::{self, Timestamp, TunnelOptions}; -use mullvad_types::wireguard::DEFAULT_ROTATION_INTERVAL; -use std::{convert::TryFrom, time::Duration}; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{ + relay_constraints::Constraint, + wireguard::{QuantumResistantState, RotationInterval, DEFAULT_ROTATION_INTERVAL}, +}; -pub struct Tunnel; +use super::BooleanOption; -#[mullvad_management_interface::async_trait] -impl Command for Tunnel { - fn name(&self) -> &'static str { - "tunnel" - } - - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Manage tunnel specific options") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_openvpn_subcommand()) - .subcommand(create_wireguard_subcommand()) - .subcommand(create_ipv6_subcommand()) - } +#[derive(Subcommand, Debug)] +pub enum Tunnel { + /// Show current tunnel options + Get, - async fn run(&self, matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("openvpn", openvpn_matches)) => Self::handle_openvpn_cmd(openvpn_matches).await, - Some(("wireguard", wg_matches)) => Self::handle_wireguard_cmd(wg_matches).await, - Some(("ipv6", ipv6_matches)) => Self::handle_ipv6_cmd(ipv6_matches).await, - _ => { - unreachable!("unhandled command"); - } - } - } + /// Set tunnel options + #[clap(subcommand)] + Set(TunnelOptions), } -fn create_wireguard_subcommand() -> clap::App<'static> { - let subcmd = clap::App::new("wireguard") - .about("Manage options for Wireguard tunnels") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_wireguard_mtu_subcommand()) - .subcommand(create_wireguard_quantum_resistant_tunnel_subcommand()) - .subcommand(create_wireguard_keys_subcommand()); - #[cfg(windows)] - { - subcmd.subcommand(create_wireguard_use_wg_nt_subcommand()) - } - #[cfg(not(windows))] - { - subcmd - } -} - -fn create_wireguard_mtu_subcommand() -> clap::App<'static> { - clap::App::new("mtu") - .about("Configure the MTU of the wireguard tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand(clap::App::new("unset")) - .subcommand(clap::App::new("set").arg(clap::Arg::new("mtu").required(true))) -} +#[derive(Subcommand, Debug, Clone)] +pub enum TunnelOptions { + /// Manage options for OpenVPN tunnels + #[clap(arg_required_else_help = true)] + Openvpn { + /// Configure the mssfix parameter, or 'any' + #[arg(long, short = 'm')] + mssfix: Option<Constraint<u16>>, + }, -fn create_wireguard_quantum_resistant_tunnel_subcommand() -> clap::App<'static> { - clap::App::new("quantum-resistant-tunnel") - .about("Controls the quantum-resistant PSK exchange in the tunnel") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("set").arg( - clap::Arg::new("policy") - .required(true) - .possible_values(["on", "off", "auto"]), - ), - ) -} + /// Manage options for WireGuard tunnels + #[clap(arg_required_else_help = true)] + Wireguard { + /// Configure the tunnel MTU, or 'any' + #[arg(long, short = 'm')] + mtu: Option<Constraint<u16>>, + /// Configure quantum-resistant key exchange + #[arg(long)] + quantum_resistant: Option<QuantumResistantState>, + /// The key rotation interval. Number of hours, or 'any' + #[arg(long)] + rotation_interval: Option<Constraint<RotationInterval>>, + /// Rotate WireGuard key + #[clap(subcommand)] + rotate_key: Option<RotateKey>, + }, -fn create_wireguard_keys_subcommand() -> clap::App<'static> { - clap::App::new("key") - .about("Manage your wireguard key") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("check")) - .subcommand(clap::App::new("regenerate")) - .subcommand(create_wireguard_keys_rotation_interval_subcommand()) + /// Enable or disable IPv6 in the tunnel + #[clap(arg_required_else_help = true)] + Ipv6 { state: BooleanOption }, } -#[cfg(windows)] -fn create_wireguard_use_wg_nt_subcommand() -> clap::App<'static> { - clap::App::new("use-wireguard-nt") - .about("Enable or disable wireguard-nt") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("set").arg( - clap::Arg::new("policy") - .required(true) - .takes_value(true) - .possible_values(["on", "off"]), - ), - ) -} - -fn create_wireguard_keys_rotation_interval_subcommand() -> clap::App<'static> { - clap::App::new("rotation-interval") - .about("Manage automatic key rotation (given in hours)") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand(clap::App::new("reset").about("Use the default rotation interval")) - .subcommand(clap::App::new("set").arg(clap::Arg::new("interval").required(true))) -} - -fn create_openvpn_subcommand() -> clap::App<'static> { - clap::App::new("openvpn") - .about("Manage options for OpenVPN tunnels") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(create_openvpn_mssfix_subcommand()) -} - -fn create_openvpn_mssfix_subcommand() -> clap::App<'static> { - clap::App::new("mssfix") - .about("Configure the optional mssfix parameter") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand(clap::App::new("unset")) - .subcommand(clap::App::new("set").arg(clap::Arg::new("mssfix").required(true))) -} - -fn create_ipv6_subcommand() -> clap::App<'static> { - clap::App::new("ipv6") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand(clap::App::new("get")) - .subcommand( - clap::App::new("set").arg( - clap::Arg::new("policy") - .required(true) - .takes_value(true) - .possible_values(["on", "off"]), - ), - ) +#[derive(Subcommand, Debug, Clone)] +pub enum RotateKey { + /// Replace the WireGuard key with a new one + RotateKey, } impl Tunnel { - async fn handle_openvpn_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("mssfix", mssfix_matches)) => { - Self::handle_openvpn_mssfix_cmd(mssfix_matches).await - } - _ => unreachable!("unhandled command"), + pub async fn handle(self) -> Result<()> { + match self { + Tunnel::Get => Self::get().await, + Tunnel::Set(options) => Self::set(options).await, } } - async fn handle_openvpn_mssfix_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("get", _)) => Self::process_openvpn_mssfix_get().await, - Some(("unset", _)) => Self::process_openvpn_mssfix_unset().await, - Some(("set", set_matches)) => Self::process_openvpn_mssfix_set(set_matches).await, - _ => unreachable!("unhandled command"), - } - } + async fn get() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let tunnel_options = rpc.get_settings().await?.tunnel_options; - async fn handle_wireguard_cmd(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("mtu", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_mtu_get().await, - Some(("set", matches)) => Self::process_wireguard_mtu_set(matches).await, - Some(("unset", _)) => Self::process_wireguard_mtu_unset().await, - _ => unreachable!("unhandled command"), - }, + println!("OpenVPN options"); - Some(("key", matches)) => match matches.subcommand() { - Some(("check", _)) => Self::process_wireguard_key_check().await, - Some(("regenerate", _)) => Self::process_wireguard_key_generate().await, - Some(("rotation-interval", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_rotation_interval_get().await, - Some(("set", matches)) => { - Self::process_wireguard_rotation_interval_set(matches).await - } - Some(("reset", _)) => Self::process_wireguard_rotation_interval_reset().await, - _ => unreachable!("unhandled command"), - }, - _ => unreachable!("unhandled command"), - }, - - Some(("quantum-resistant-tunnel", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_quantum_resistant_tunnel_get().await, - Some(("set", matches)) => { - Self::process_wireguard_quantum_resistant_tunnel_set(matches).await - } - _ => unreachable!("unhandled command"), - }, + println!( + "{:<4}{:<24}{}", + "", + "mssfix:", + tunnel_options + .openvpn + .mssfix + .map(|val| val.to_string()) + .unwrap_or("unset".to_string()), + ); - #[cfg(windows)] - Some(("use-wireguard-nt", matches)) => match matches.subcommand() { - Some(("get", _)) => Self::process_wireguard_use_wg_nt_get().await, - Some(("set", matches)) => Self::process_wireguard_use_wg_nt_set(matches).await, - _ => unreachable!("unhandled command"), - }, + println!("WireGuard options"); - _ => unreachable!("unhandled command"), - } - } + println!( + "{:<4}{:<24}{}", + "", + "MTU:", + tunnel_options + .wireguard + .mtu + .map(|val| val.to_string()) + .unwrap_or("unset".to_string()), + ); + println!( + "{:<4}{:<24}{}", + "", "Quantum resistance:", tunnel_options.wireguard.quantum_resistant, + ); - async fn process_wireguard_mtu_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - let mtu = tunnel_options.wireguard.unwrap().mtu; + let key = rpc.get_wireguard_key().await?; + println!("{:<4}{:<24}{}", "", "Public key:", key.key,); println!( - "mtu: {}", - if mtu != 0 { - mtu.to_string() - } else { - "unset".to_string() + "{:<4}{:<24}{}", + "", + "", + format_args!("Created {}", key.created.with_timezone(&chrono::Local)), + ); + println!( + "{:<4}{:<24}{}", + "", + "Rotation interval:", + match tunnel_options.wireguard.rotation_interval { + Some(interval) => interval.to_string(), + None => "unset".to_string(), }, ); - Ok(()) - } - - async fn process_wireguard_mtu_set(matches: &clap::ArgMatches) -> Result<()> { - let mtu = matches.value_of_t_or_exit::<u16>("mtu"); - let mut rpc = new_rpc_client().await?; - rpc.set_wireguard_mtu(mtu as u32).await?; - println!("Wireguard MTU has been updated"); - Ok(()) - } - - async fn process_wireguard_mtu_unset() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_wireguard_mtu(0).await?; - println!("Wireguard MTU has been unset"); - Ok(()) - } - - async fn process_wireguard_quantum_resistant_tunnel_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - match tunnel_options - .wireguard - .unwrap() - .quantum_resistant - .and_then(|state| types::quantum_resistant_state::State::from_i32(state.state)) - { - Some(types::quantum_resistant_state::State::On) => println!("enabled"), - Some(types::quantum_resistant_state::State::Off) => println!("disabled"), - None | Some(types::quantum_resistant_state::State::Auto) => println!("auto"), - } - Ok(()) - } - async fn process_wireguard_quantum_resistant_tunnel_set( - matches: &clap::ArgMatches, - ) -> Result<()> { - let quantum_resistant = match matches.value_of("policy").unwrap() { - "auto" => types::quantum_resistant_state::State::Auto, - "on" => types::quantum_resistant_state::State::On, - "off" => types::quantum_resistant_state::State::Off, - _ => unreachable!("invalid PQ state"), - }; - let mut rpc = new_rpc_client().await?; - rpc.set_quantum_resistant_tunnel(types::QuantumResistantState { - state: i32::from(quantum_resistant), - }) - .await?; - println!("Updated quantum resistant tunnel setting"); - Ok(()) - } + println!("Generic options"); - #[cfg(windows)] - async fn process_wireguard_use_wg_nt_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - if tunnel_options.wireguard.unwrap().use_wireguard_nt { - println!("enabled"); + if tunnel_options.generic.enable_ipv6 { + println!("{:<4}{:<24}on", "", "IPv6:"); } else { - println!("disabled"); + println!("{:<4}{:<24}off", "", "IPv6:"); } - Ok(()) - } - #[cfg(windows)] - async fn process_wireguard_use_wg_nt_set(matches: &clap::ArgMatches) -> Result<()> { - let new_state = matches.value_of("policy").unwrap() == "on"; - let mut rpc = new_rpc_client().await?; - rpc.set_use_wireguard_nt(new_state).await?; - println!("Updated wireguard-nt setting"); Ok(()) } - async fn process_wireguard_key_check() -> Result<()> { - let mut rpc = new_rpc_client().await?; - let key = rpc.get_wireguard_key(()).await; - let key = match key { - Ok(response) => Some(response.into_inner()), - Err(status) => { - if status.code() == mullvad_management_interface::Code::NotFound { - None - } else { - return Err(Error::RpcFailedExt("Failed to obtain key", status)); - } + async fn set(options: TunnelOptions) -> Result<()> { + match options { + TunnelOptions::Openvpn { mssfix } => Self::handle_openvpn(mssfix).await, + TunnelOptions::Wireguard { + mtu, + quantum_resistant, + rotation_interval, + rotate_key, + } => { + Self::handle_wireguard(mtu, quantum_resistant, rotation_interval, rotate_key).await } - }; - if let Some(key) = key { - println!("Current key : {}", base64::encode(&key.key)); - println!( - "Key created on : {}", - Self::format_key_timestamp(&key.created.unwrap()) - ); - } else { - println!("No key is set"); - return Ok(()); + TunnelOptions::Ipv6 { state } => Self::handle_ipv6(state).await, } - Ok(()) } - async fn process_wireguard_key_generate() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.rotate_wireguard_key(()).await?; - println!("Rotated WireGuard key"); + async fn handle_ipv6(state: BooleanOption) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.set_enable_ipv6(*state).await?; + println!("IPv6: {state}"); Ok(()) } - async fn process_wireguard_rotation_interval_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - match tunnel_options.wireguard.unwrap().rotation_interval { - Some(interval) => { - let hours = duration_hours(&Duration::try_from(interval).unwrap()); - println!("Rotation interval: {hours} hour(s)"); - } - None => println!( - "Rotation interval: default ({} hours)", - duration_hours(&DEFAULT_ROTATION_INTERVAL) - ), - } - Ok(()) - } - - async fn process_wireguard_rotation_interval_set(matches: &clap::ArgMatches) -> Result<()> { - let rotate_interval = matches.value_of_t_or_exit::<u64>("interval"); - let mut rpc = new_rpc_client().await?; - rpc.set_wireguard_rotation_interval( - types::Duration::try_from(Duration::from_secs(60 * 60 * rotate_interval)) - .expect("Failed to convert rotation interval to prost_types::Duration"), - ) - .await?; - println!("Set key rotation interval: {rotate_interval} hour(s)"); - Ok(()) - } - - async fn process_wireguard_rotation_interval_reset() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.reset_wireguard_rotation_interval(()).await?; - println!( - "Set key rotation interval: default ({} hours)", - duration_hours(&DEFAULT_ROTATION_INTERVAL) - ); - Ok(()) - } + async fn handle_openvpn(mssfix: Option<Constraint<u16>>) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; - async fn handle_ipv6_cmd(matches: &clap::ArgMatches) -> Result<()> { - if matches.subcommand_matches("get").is_some() { - Self::process_ipv6_get().await - } else if let Some(m) = matches.subcommand_matches("set") { - Self::process_ipv6_set(m).await - } else { - unreachable!("unhandled command"); + if let Some(mssfix) = mssfix { + rpc.set_openvpn_mssfix(mssfix.option()).await?; + println!("mssfix parameter has been updated"); } - } - async fn process_openvpn_mssfix_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - let mssfix = tunnel_options.openvpn.unwrap().mssfix; - println!( - "mssfix: {}", - if mssfix != 0 { - mssfix.to_string() - } else { - "unset".to_string() - }, - ); Ok(()) } - async fn get_tunnel_options() -> Result<TunnelOptions> { - let mut rpc = new_rpc_client().await?; - Ok(rpc - .get_settings(()) - .await? - .into_inner() - .tunnel_options - .unwrap()) - } + async fn handle_wireguard( + mtu: Option<Constraint<u16>>, + quantum_resistant: Option<QuantumResistantState>, + rotation_interval: Option<Constraint<RotationInterval>>, + rotate_key: Option<RotateKey>, + ) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; - async fn process_openvpn_mssfix_unset() -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_openvpn_mssfix(0).await?; - println!("mssfix parameter has been unset"); - Ok(()) - } + if let Some(mtu) = mtu { + rpc.set_wireguard_mtu(mtu.option()).await?; + println!("MTU parameter has been updated"); + } - async fn process_openvpn_mssfix_set(matches: &clap::ArgMatches) -> Result<()> { - let new_value = matches.value_of_t_or_exit::<u16>("mssfix"); - let mut rpc = new_rpc_client().await?; - rpc.set_openvpn_mssfix(new_value as u32).await?; - println!("mssfix parameter has been updated"); - Ok(()) - } + if let Some(quantum_resistant) = quantum_resistant { + rpc.set_quantum_resistant_tunnel(quantum_resistant).await?; + println!("Quantum resistant setting has been updated"); + } - async fn process_ipv6_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options().await?; - println!( - "IPv6: {}", - if tunnel_options.generic.unwrap().enable_ipv6 { - "on" - } else { - "off" + if let Some(interval) = rotation_interval { + match interval { + Constraint::Only(interval) => { + rpc.set_wireguard_rotation_interval(interval).await?; + println!("Set key rotation interval to {}", interval); + } + Constraint::Any => { + rpc.reset_wireguard_rotation_interval().await?; + println!( + "Reset key rotation interval to {}", + RotationInterval::new(DEFAULT_ROTATION_INTERVAL).unwrap() + ); + } } - ); - Ok(()) - } - - async fn process_ipv6_set(matches: &clap::ArgMatches) -> Result<()> { - let enabled = matches.value_of("policy").unwrap() == "on"; + } - let mut rpc = new_rpc_client().await?; - rpc.set_enable_ipv6(enabled).await?; - if enabled { - println!("Enabled IPv6"); - } else { - println!("Disabled IPv6"); + if matches!(rotate_key, Some(RotateKey::RotateKey)) { + rpc.rotate_wireguard_key().await?; + println!("Rotated WireGuard key"); } - Ok(()) - } - fn format_key_timestamp(timestamp: &Timestamp) -> String { - let ndt = chrono::NaiveDateTime::from_timestamp(timestamp.seconds, timestamp.nanos as u32); - let utc = chrono::DateTime::<chrono::Utc>::from_utc(ndt, chrono::Utc); - utc.with_timezone(&chrono::Local).to_string() + Ok(()) } } - -fn duration_hours(duration: &Duration) -> u64 { - duration.as_secs() / 60 / 60 -} diff --git a/mullvad-cli/src/cmds/tunnel_state.rs b/mullvad-cli/src/cmds/tunnel_state.rs new file mode 100644 index 0000000000..383b343ef4 --- /dev/null +++ b/mullvad-cli/src/cmds/tunnel_state.rs @@ -0,0 +1,85 @@ +use crate::format; +use anyhow::{anyhow, Result}; +use futures::{Stream, StreamExt}; +use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; +use mullvad_types::states::TunnelState; + +pub async fn connect(wait: bool) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let listener = if wait { + Some(rpc.events_listen().await?) + } else { + None + }; + + if rpc.connect_tunnel().await? { + if let Some(receiver) = listener { + wait_for_tunnel_state(receiver, |state| match state { + TunnelState::Connected { .. } => Ok(true), + TunnelState::Error(_) => Err(anyhow!("Failed to connect")), + _ => Ok(false), + }) + .await?; + } + } + + Ok(()) +} + +pub async fn disconnect(wait: bool) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let listener = if wait { + Some(rpc.events_listen().await?) + } else { + None + }; + + if rpc.disconnect_tunnel().await? { + if let Some(receiver) = listener { + wait_for_tunnel_state(receiver, |state| Ok(state.is_disconnected())).await?; + } + } + + Ok(()) +} + +pub async fn reconnect(wait: bool) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + + let listener = if wait { + Some(rpc.events_listen().await?) + } else { + None + }; + + if rpc.reconnect_tunnel().await? { + if let Some(receiver) = listener { + wait_for_tunnel_state(receiver, |state| match state { + TunnelState::Connected { .. } => Ok(true), + TunnelState::Error(_) => Err(anyhow!("Failed to reconnect")), + _ => Ok(false), + }) + .await?; + } + } + + Ok(()) +} + +async fn wait_for_tunnel_state( + mut event_stream: impl Stream<Item = std::result::Result<DaemonEvent, mullvad_management_interface::Error>> + + Unpin, + matches_event: impl Fn(&TunnelState) -> Result<bool>, +) -> Result<()> { + while let Some(state) = event_stream.next().await { + if let DaemonEvent::TunnelState(new_state) = state? { + format::print_state(&new_state, false); + if matches_event(&new_state)? { + return Ok(()); + } + } + } + Err(anyhow!("Failed to wait for expected tunnel state")) +} diff --git a/mullvad-cli/src/cmds/version.rs b/mullvad-cli/src/cmds/version.rs index 2f034eff32..9a0cce2f41 100644 --- a/mullvad-cli/src/cmds/version.rs +++ b/mullvad-cli/src/cmds/version.rs @@ -1,54 +1,39 @@ -use crate::{new_rpc_client, Command, Error, Result}; +use anyhow::{Context, Result}; +use mullvad_management_interface::MullvadProxyClient; -pub struct Version; +pub async fn print() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let current_version = rpc + .get_current_version() + .await + .context("Failed to get current version")?; + println!("{:21}: {}", "Current version", current_version); + let version_info = rpc + .get_version_info() + .await + .context("Failed to get version info")?; + println!("{:21}: {}", "Is supported", version_info.supported); -#[mullvad_management_interface::async_trait] -impl Command for Version { - fn name(&self) -> &'static str { - "version" + if let Some(suggested_upgrade) = version_info.suggested_upgrade { + println!("{:21}: {}", "Suggested upgrade", suggested_upgrade); + } else { + println!("{:21}: none", "Suggested upgrade"); } - fn clap_subcommand(&self) -> clap::App<'static> { - clap::App::new(self.name()) - .about("Shows current version, and the currently supported versions") + if !version_info.latest_stable.is_empty() { + println!( + "{:21}: {}", + "Latest stable version", version_info.latest_stable + ); } - async fn run(&self, _: &clap::ArgMatches) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let current_version = rpc - .get_current_version(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain current version", error))? - .into_inner(); - println!("{:21}: {}", "Current version", current_version); - let version_info = rpc - .get_version_info(()) - .await - .map_err(|error| Error::RpcFailedExt("Failed to obtain version info", error))? - .into_inner(); - println!("{:21}: {}", "Is supported", version_info.supported); + let settings = rpc + .get_settings() + .await + .context("Failed to obtain settings")?; + if settings.show_beta_releases { + println!("{:21}: {}", "Latest beta version", version_info.latest_beta); + }; - if !version_info.suggested_upgrade.is_empty() { - println!( - "{:21}: {}", - "Suggested upgrade", version_info.suggested_upgrade - ); - } else { - println!("{:21}: none", "Suggested upgrade"); - } - - if !version_info.latest_stable.is_empty() { - println!( - "{:21}: {}", - "Latest stable version", version_info.latest_stable - ); - } - - let settings = rpc.get_settings(()).await?.into_inner(); - if settings.show_beta_releases { - println!("{:21}: {}", "Latest beta version", version_info.latest_beta); - }; - - Ok(()) - } + Ok(()) } diff --git a/mullvad-cli/src/location.rs b/mullvad-cli/src/location.rs deleted file mode 100644 index 2fff68998c..0000000000 --- a/mullvad-cli/src/location.rs +++ /dev/null @@ -1,81 +0,0 @@ -use mullvad_management_interface::types::RelayLocation; - -pub fn get_subcommand() -> clap::App<'static> { - clap::App::new("location") - .arg( - clap::Arg::new("country") - .help("The two letter country code, or 'any' for no preference.") - .required(true) - .index(1) - .validator(country_code_validator), - ) - .arg( - clap::Arg::new("city") - .help("The three letter city code") - .index(2) - .validator(city_code_validator), - ) - .arg(clap::Arg::new("hostname").help("The hostname").index(3)) -} - -pub fn get_constraint_from_args(matches: &clap::ArgMatches) -> RelayLocation { - let country = matches.value_of("country").unwrap(); - let city = matches.value_of("city"); - let hostname = matches.value_of("hostname"); - get_constraint(country, city, hostname) -} - -pub fn get_constraint<T: AsRef<str>>( - country: T, - city: Option<T>, - hostname: Option<T>, -) -> RelayLocation { - let country_original = country.as_ref(); - let country = country_original.to_lowercase(); - let city = city.map(|s| s.as_ref().to_lowercase()); - let hostname = hostname.map(|s| s.as_ref().to_lowercase()); - - match (country_original, city, hostname) { - ("any", None, None) => RelayLocation::default(), - ("any", ..) => clap::Error::raw( - clap::ErrorKind::InvalidValue, - "City can't be given when selecting 'any' country", - ) - .exit(), - (_, None, None) => RelayLocation { - country, - ..Default::default() - }, - (_, Some(city), None) => RelayLocation { - country, - city, - ..Default::default() - }, - (_, Some(city), Some(hostname)) => RelayLocation { - country, - city, - hostname, - }, - (..) => clap::Error::raw( - clap::ErrorKind::InvalidValue, - "Invalid country, city and hostname combination given", - ) - .exit(), - } -} - -pub fn country_code_validator(code: &str) -> std::result::Result<(), String> { - if code.len() == 2 || code == "any" { - Ok(()) - } else { - Err(String::from("Country codes must be two letters, or 'any'.")) - } -} - -pub fn city_code_validator(code: &str) -> std::result::Result<(), String> { - if code.len() == 3 { - Ok(()) - } else { - Err(String::from("City codes must be three letters")) - } -} diff --git a/mullvad-cli/src/main.rs b/mullvad-cli/src/main.rs index 39479d4054..a36ec58a81 100644 --- a/mullvad-cli/src/main.rs +++ b/mullvad-cli/src/main.rs @@ -1,150 +1,152 @@ #![deny(rust_2018_idioms)] -use clap::{crate_authors, crate_description}; #[cfg(all(unix, not(target_os = "android")))] -use clap_complete::{generator::generate_to, Shell}; -use mullvad_management_interface::async_trait; -use std::{collections::HashMap, io}; -use talpid_types::ErrorExt; - -pub use mullvad_management_interface::{self, new_rpc_client}; +use anyhow::anyhow; +use anyhow::Result; +use clap::Parser; mod cmds; mod format; -mod location; -mod state; +use cmds::*; -pub const BIN_NAME: &str = "mullvad"; +pub const BIN_NAME: &str = env!("CARGO_BIN_NAME"); -pub type Result<T> = std::result::Result<T, Error>; +#[derive(Debug, Parser)] +#[command(author, version = mullvad_version::VERSION, about, long_about = None)] +#[command(propagate_version = true)] +enum Cli { + /// Control and display information about your Mullvad account + #[clap(subcommand)] + Account(account::Account), -#[derive(err_derive::Error, Debug)] -pub enum Error { - #[error(display = "Failed to connect to daemon")] - DaemonNotRunning(#[error(source)] io::Error), + /// Control the daemon auto-connect setting + #[clap(subcommand)] + AutoConnect(auto_connect::AutoConnect), - #[error(display = "Management interface error")] - ManagementInterfaceError(#[error(source)] mullvad_management_interface::Error), + /// Receive notifications about beta updates + #[clap(subcommand)] + BetaProgram(beta_program::BetaProgram), - #[error(display = "RPC failed")] - RpcFailed(#[error(source)] mullvad_management_interface::Status), + /// Control whether to block network access when disconnected from VPN + #[clap(subcommand)] + LockdownMode(lockdown::LockdownMode), - #[error(display = "RPC failed: {}", _0)] - RpcFailedExt( - &'static str, - #[error(source)] mullvad_management_interface::Status, - ), + /// Configure DNS servers to use when connected + #[clap(subcommand)] + Dns(dns::Dns), - /// The given command is not correct in some way - #[error(display = "Invalid command: {}", _0)] - InvalidCommand(&'static str), + /// Control the allow local network sharing setting + #[clap(subcommand)] + Lan(lan::Lan), - #[error(display = "Command failed: {}", _0)] - CommandFailed(&'static str), + /// Connect to a VPN relay + Connect { + /// Wait until connected before exiting + #[arg(long, short = 'w')] + wait: bool, + }, - #[error(display = "Failed to listen for status updates")] - StatusListenerFailed, + /// Disconnect from the VPN + Disconnect { + /// Wait until disconnected before exiting + #[arg(long, short = 'w')] + wait: bool, + }, - //#[cfg(all(unix, not(target_os = "android")) - #[error(display = "Failed to generate shell completions")] - CompletionsError(#[error(source, no_from)] io::Error), + /// Reconnect to any matching VPN relay + Reconnect { + /// Wait until connected before exiting + #[arg(long, short = 'w')] + wait: bool, + }, - #[error(display = "{}", _0)] - Other(&'static str), -} + /// Manage use of bridges, socks proxies and Shadowsocks for OpenVPN. + /// Can make OpenVPN tunnels use Shadowsocks via one of the Mullvad bridge servers. + /// Can also make OpenVPN connect through any custom SOCKS5 proxy. + /// These settings also affect how the app reaches the API over Shadowsocks. + #[clap(subcommand)] + Bridge(bridge::Bridge), -#[tokio::main] -async fn main() { - let exit_code = match run().await { - Ok(_) => 0, - Err(error) => { - match &error { - Error::RpcFailed(status) => { - eprintln!("{}: {:?}: {}", error, status.code(), status.message()) - } - Error::RpcFailedExt(_message, status) => eprintln!( - "{}\nCaused by: {:?}: {}", - error, - status.code(), - status.message() - ), - error => eprintln!("{}", error.display_chain()), - } - 1 - } - }; - std::process::exit(exit_code); -} + /// Manage relay and tunnel constraints + #[clap(subcommand)] + Relay(relay::Relay), -async fn run() -> Result<()> { - env_logger::init(); + /// Manage use of obfuscation protocols for WireGuard. + /// Can make WireGuard traffic look like something else on the network. + /// Helps circumvent censorship and to establish a tunnel when on restricted networks + #[clap(subcommand)] + Obfuscation(obfuscation::Obfuscation), - let commands = cmds::get_commands(); - let app = build_cli(&commands); + #[cfg(any(target_os = "windows", target_os = "linux"))] + #[clap(subcommand)] + SplitTunnel(split_tunnel::SplitTunnel), + /// Return the state of the VPN tunnel + Status { + #[clap(subcommand)] + cmd: Option<status::Status>, + + #[clap(flatten)] + args: status::StatusArgs, + }, + + /// Manage tunnel options + #[clap(subcommand)] + Tunnel(tunnel::Tunnel), + + /// Show information about the current Mullvad version + /// and available versions + Version, + + /// Generate completion scripts for the specified shell #[cfg(all(unix, not(target_os = "android")))] - let app = app.subcommand( - clap::App::new("shell-completions") - .about("Generates completion scripts for your shell") - .arg( - clap::Arg::new("SHELL") - .required(true) - .possible_values(Shell::possible_values()) - .help("The shell to generate the script for"), - ) - .arg( - clap::Arg::new("DIR") - .allow_invalid_utf8(true) - .default_value("./") - .help("Output directory where the shell completions are written"), - ) - .setting(clap::AppSettings::Hidden), - ); + #[command(hide = true)] + ShellCompletions { + /// The shell to generate the script for + shell: clap_complete::Shell, - let app_matches = app.get_matches(); - match app_matches.subcommand() { - #[cfg(all(unix, not(target_os = "android")))] - Some(("shell-completions", sub_matches)) => { - let shell: Shell = sub_matches - .value_of("SHELL") - .unwrap() - .parse() - .expect("Invalid shell"); - let out_dir = sub_matches.value_of_os("DIR").unwrap(); - let mut app = build_cli(&commands); - generate_to(shell, &mut app, BIN_NAME, out_dir) - .map(|_output_file| ()) - .map_err(Error::CompletionsError) - } - Some((sub_name, sub_matches)) => { - if let Some(cmd) = commands.get(sub_name) { - cmd.run(sub_matches).await - } else { - unreachable!("No command matched"); - } - } - _ => { - unreachable!("No subcommand matches"); - } - } -} + /// Output directory where the shell completions are written + #[arg(default_value = "./")] + dir: std::path::PathBuf, + }, -fn build_cli(commands: &HashMap<&'static str, Box<dyn Command>>) -> clap::App<'static> { - clap::App::new(BIN_NAME) - .version(mullvad_version::VERSION) - .author(crate_authors!()) - .about(crate_description!()) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .global_setting(clap::AppSettings::DisableHelpSubcommand) - .global_setting(clap::AppSettings::DisableVersionFlag) - .subcommands(commands.values().map(|cmd| cmd.clap_subcommand())) + /// Reset settings, caches, and logs + FactoryReset, } -#[async_trait] -pub trait Command { - fn name(&self) -> &'static str; +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + + match Cli::parse() { + Cli::Account(cmd) => cmd.handle().await, + Cli::Bridge(cmd) => cmd.handle().await, + Cli::Connect { wait } => tunnel_state::connect(wait).await, + Cli::Reconnect { wait } => tunnel_state::reconnect(wait).await, + Cli::Disconnect { wait } => tunnel_state::disconnect(wait).await, + Cli::AutoConnect(cmd) => cmd.handle().await, + Cli::BetaProgram(cmd) => cmd.handle().await, + Cli::LockdownMode(cmd) => cmd.handle().await, + Cli::Dns(cmd) => cmd.handle().await, + Cli::Lan(cmd) => cmd.handle().await, + Cli::Obfuscation(cmd) => cmd.handle().await, + Cli::Version => version::print().await, + Cli::FactoryReset => reset::handle().await, + Cli::Relay(cmd) => cmd.handle().await, + Cli::Tunnel(cmd) => cmd.handle().await, + #[cfg(any(target_os = "windows", target_os = "linux"))] + Cli::SplitTunnel(cmd) => cmd.handle().await, + Cli::Status { cmd, args } => status::handle(cmd, args).await, - fn clap_subcommand(&self) -> clap::App<'static>; + #[cfg(all(unix, not(target_os = "android")))] + Cli::ShellCompletions { shell, dir } => { + use clap::CommandFactory; - async fn run(&self, matches: &clap::ArgMatches) -> Result<()>; + // FIXME: The shell completions include hidden commands (including "shell-completions") + println!("Generating shell completions to {}", dir.display()); + clap_complete::generate_to(shell, &mut Cli::command(), BIN_NAME, dir) + .map_err(|_| anyhow!("Failed to generate shell completions"))?; + Ok(()) + } + } } diff --git a/mullvad-cli/src/state.rs b/mullvad-cli/src/state.rs deleted file mode 100644 index 7b3dfdc955..0000000000 --- a/mullvad-cli/src/state.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::{Error, Result}; -use futures::{ - channel::{mpsc, mpsc::Receiver}, - SinkExt, -}; -use mullvad_management_interface::{ - types::daemon_event::Event as EventType, ManagementServiceClient, -}; -use mullvad_types::states::TunnelState; - -// Spawns a new task that listens for tunnel state changes and forwards it through the returned -// channel. Panics if called from outside of the Tokio runtime. -pub fn state_listen(mut rpc: ManagementServiceClient) -> Receiver<Result<TunnelState>> { - let (mut sender, receiver) = mpsc::channel::<Result<TunnelState>>(1); - tokio::spawn(async move { - match rpc.events_listen(()).await { - Ok(events) => { - let mut events = events.into_inner(); - loop { - let forward = match events.message().await { - Ok(Some(event)) => match event.event.unwrap() { - EventType::TunnelState(new_state) => { - Ok(TunnelState::try_from(new_state).expect("invalid tunnel state")) - } - _ => continue, - }, - Ok(None) => break, - Err(status) => Err(Error::RpcFailed(status)), - }; - - if sender.send(forward).await.is_err() { - break; - } - } - } - Err(status) => { - let _ = sender.send(Err(Error::RpcFailed(status))).await; - } - } - }); - - receiver -} diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index 9c71d57eba..709a061326 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -37,7 +37,7 @@ talpid-platform-metadata = { path = "../talpid-platform-metadata" } talpid-time = { path = "../talpid-time" } [target.'cfg(not(target_os="android"))'.dependencies] -clap = { version = "3.0", features = ["cargo"] } +clap = { version = "4.2.7", features = ["cargo"] } log-panics = "2.0.0" mullvad-management-interface = { path = "../mullvad-management-interface" } mullvad-paths = { path = "../mullvad-paths" } diff --git a/mullvad-daemon/src/cli.rs b/mullvad-daemon/src/cli.rs index 59223cbc17..00552a611a 100644 --- a/mullvad-daemon/src/cli.rs +++ b/mullvad-daemon/src/cli.rs @@ -1,11 +1,67 @@ -use clap::{crate_authors, crate_description, crate_name, App, Arg}; +use clap::Parser; + +lazy_static::lazy_static! { + static ref ENV_DESC: String = format!( +"ENV: + + MULLVAD_RESOURCE_DIR Resource directory (i.e used to locate a root CA certificate) + [Default: {}] + MULLVAD_SETTINGS_DIR Directory path for storing settings. [Default: {}] + MULLVAD_CACHE_DIR Directory path for storing cache. [Default: {}] + MULLVAD_LOG_DIR Directory path for storing logs. [Default: {}] + MULLVAD_RPC_SOCKET_PATH Location of the management interface device. + It refers to Unix domain socket on Unix based platforms, and named pipe on Windows. + [Default: {}] + +", + mullvad_paths::get_default_resource_dir().display(), + mullvad_paths::get_default_settings_dir().map(|dir| dir.display().to_string()).unwrap_or_else(|_| "N/A".to_string()), + mullvad_paths::get_default_cache_dir().map(|dir| dir.display().to_string()).unwrap_or_else(|_| "N/A".to_string()), + mullvad_paths::get_default_log_dir().map(|dir| dir.display().to_string()).unwrap_or_else(|_| "N/A".to_string()), + mullvad_paths::get_default_rpc_socket_path().display()); +} + +#[derive(Debug, Parser)] +#[command(author, version = mullvad_version::VERSION, about, long_about = None, after_help = &*ENV_DESC)] +struct Cli { + /// Set the level of verbosity + #[arg(short='v', action = clap::ArgAction::Count)] + verbosity: u8, + /// Disable logging to file + #[arg(long)] + disable_log_to_file: bool, + /// Don't log timestamps when logging to stdout, useful when running as a systemd service + #[arg(long)] + disable_stdout_timestamps: bool, + + /// Run as a system service + #[cfg(target_os = "windows")] + #[arg(long)] + run_as_service: bool, + /// Register Mullvad daemon as a system service + #[cfg(target_os = "windows")] + #[arg(long)] + register_service: bool, + + /// Initialize firewall to be used during early boot and exit + #[cfg(target_os = "linux")] + #[arg(long)] + initialize_early_boot_firewall: bool, + + /// Check the status of the launch daemon. The exit code represents the current status + #[cfg(target_os = "macos")] + #[arg(long)] + launch_daemon_status: bool, +} #[derive(Debug)] pub struct Config { pub log_level: log::LevelFilter, pub log_to_file: bool, pub log_stdout_timestamps: bool, + #[cfg(target_os = "windows")] pub run_as_service: bool, + #[cfg(target_os = "windows")] pub register_service: bool, #[cfg(target_os = "macos")] pub launch_daemon_status: bool, @@ -20,108 +76,26 @@ pub fn get_config() -> &'static Config { &CONFIG } -pub fn create_config() -> Config { - let app = create_app(); - let matches = app.get_matches(); +fn create_config() -> Config { + let app = Cli::parse(); - let log_level = match matches.occurrences_of("v") { + let log_level = match app.verbosity { 0 => log::LevelFilter::Info, 1 => log::LevelFilter::Debug, _ => log::LevelFilter::Trace, }; - let log_to_file = !matches.is_present("disable_log_to_file"); - let log_stdout_timestamps = !matches.is_present("disable_stdout_timestamps"); - - #[cfg(target_os = "linux")] - let initialize_firewall_and_exit = - cfg!(target_os = "linux") && matches.is_present("initialize-early-boot-firewall"); - let run_as_service = cfg!(windows) && matches.is_present("run_as_service"); - let register_service = cfg!(windows) && matches.is_present("register_service"); - #[cfg(target_os = "macos")] - let launch_daemon_status = matches.is_present("launch_daemon_status"); Config { - #[cfg(target_os = "linux")] - initialize_firewall_and_exit, log_level, - log_to_file, - log_stdout_timestamps, - run_as_service, - register_service, + log_to_file: !app.disable_log_to_file, + log_stdout_timestamps: !app.disable_stdout_timestamps, + #[cfg(target_os = "windows")] + run_as_service: app.run_as_service, + #[cfg(target_os = "windows")] + register_service: app.register_service, #[cfg(target_os = "macos")] - launch_daemon_status, - } -} - -lazy_static::lazy_static! { - static ref ENV_DESC: String = format!( -"ENV: - - MULLVAD_RESOURCE_DIR Resource directory (i.e used to locate a root CA certificate) - [Default: {}] - MULLVAD_SETTINGS_DIR Directory path for storing settings. [Default: {}] - MULLVAD_CACHE_DIR Directory path for storing cache. [Default: {}] - MULLVAD_LOG_DIR Directory path for storing logs. [Default: {}] - MULLVAD_RPC_SOCKET_PATH Location of the management interface device. - It refers to Unix domain socket on Unix based platforms, and named pipe on Windows. - [Default: {}] - -", - mullvad_paths::get_default_resource_dir().display(), - mullvad_paths::get_default_settings_dir().map(|dir| dir.display().to_string()).unwrap_or_else(|_| "N/A".to_string()), - mullvad_paths::get_default_cache_dir().map(|dir| dir.display().to_string()).unwrap_or_else(|_| "N/A".to_string()), - mullvad_paths::get_default_log_dir().map(|dir| dir.display().to_string()).unwrap_or_else(|_| "N/A".to_string()), - mullvad_paths::get_default_rpc_socket_path().display()); -} - -fn create_app() -> App<'static> { - let mut app = App::new(crate_name!()) - .version(mullvad_version::VERSION) - .author(crate_authors!(", ")) - .about(crate_description!()) - .after_help(ENV_DESC.as_str()) - .arg( - Arg::new("v") - .short('v') - .multiple_occurrences(true) - .help("Sets the level of verbosity"), - ) - .arg( - Arg::new("disable_log_to_file") - .long("disable-log-to-file") - .help("Disable logging to file"), - ) - .arg( - Arg::new("disable_stdout_timestamps") - .long("disable-stdout-timestamps") - .help("Don't log timestamps when logging to stdout, useful when running as a systemd service") - ); - - if cfg!(windows) { - app = app.arg( - Arg::new("run_as_service") - .long("run-as-service") - .help("Run as a system service. On Windows this option must be used when running a system service"), - ) - .arg( - Arg::new("register_service") - .long("register-service") - .help("Register itself as a system service"), - ) - } - - if cfg!(target_os = "linux") { - app = app.arg( - Arg::new("initialize-early-boot-firewall") - .long("initialize-early-boot-firewall") - .help("Initialize firewall to be used during early boot and exit"), - ) - } - - if cfg!(target_os = "macos") { - app = app.arg(Arg::new("launch_daemon_status").long("launch-daemon-status").help( - "Checks the status of the launch daemon. The exit code represents the current status", - )) + launch_daemon_status: app.launch_daemon_status, + #[cfg(target_os = "linux")] + initialize_firewall_and_exit: app.initialize_early_boot_firewall, } - app } diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 3e18341818..28b33c908d 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -72,6 +72,8 @@ use talpid_core::{ }; #[cfg(target_os = "android")] use talpid_types::android::AndroidContext; +#[cfg(target_os = "windows")] +use talpid_types::split_tunnel::ExcludedProcess; use talpid_types::{ net::{TunnelEndpoint, TunnelType}, tunnel::{ErrorStateCause, TunnelStateTransition}, @@ -275,7 +277,7 @@ pub enum DaemonCommand { SetSplitTunnelState(ResponseTx<(), Error>, bool), /// Returns all processes currently being excluded from the tunnel #[cfg(windows)] - GetSplitTunnelProcesses(ResponseTx<Vec<split_tunnel::ExcludedProcess>, split_tunnel::Error>), + GetSplitTunnelProcesses(ResponseTx<Vec<ExcludedProcess>, split_tunnel::Error>), /// Toggle wireguard-nt on or off #[cfg(target_os = "windows")] UseWireGuardNt(ResponseTx<(), Error>, bool), @@ -1715,7 +1717,7 @@ where #[cfg(windows)] fn on_get_split_tunnel_processes( &self, - tx: ResponseTx<Vec<split_tunnel::ExcludedProcess>, split_tunnel::Error>, + tx: ResponseTx<Vec<ExcludedProcess>, split_tunnel::Error>, ) { Self::oneshot_send( tx, diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index ad2b556bcb..76ebc56612 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -423,14 +423,7 @@ impl ManagementService for ManagementServiceImpl { self.send_command_to_daemon(DaemonCommand::GetAccountData(tx, account_token))?; let result = self.wait_for_result(rx).await?; result - .map(|account_data| { - Response::new(types::AccountData { - expiry: Some(types::Timestamp { - seconds: account_data.expiry.timestamp(), - nanos: 0, - }), - }) - }) + .map(|account_data| Response::new(types::AccountData::from(account_data))) .map_err(|error: RestError| { log::error!( "Unable to get account data from API: {}", @@ -483,15 +476,7 @@ impl ManagementService for ManagementServiceImpl { self.send_command_to_daemon(DaemonCommand::SubmitVoucher(tx, voucher))?; let result = self.wait_for_result(rx).await?; result - .map(|submission| { - Response::new(types::VoucherSubmission { - seconds_added: submission.time_added, - new_expiry: Some(types::Timestamp { - seconds: submission.new_expiry.timestamp(), - nanos: 0, - }), - }) - }) + .map(|submission| Response::new(types::VoucherSubmission::from(submission))) .map_err(map_daemon_error) } @@ -756,11 +741,7 @@ impl ManagementService for ManagementServiceImpl { Response::new(types::ExcludedProcessList { processes: processes .into_iter() - .map(|process| types::ExcludedProcess { - pid: process.pid, - image: process.image.into_os_string().to_string_lossy().to_string(), - inherited: process.inherited, - }) + .map(types::ExcludedProcess::from) .collect(), }) }) diff --git a/mullvad-daemon/src/rpc_uniqueness_check.rs b/mullvad-daemon/src/rpc_uniqueness_check.rs index 76d524c765..89e08eb47f 100644 --- a/mullvad-daemon/src/rpc_uniqueness_check.rs +++ b/mullvad-daemon/src/rpc_uniqueness_check.rs @@ -1,4 +1,4 @@ -use mullvad_management_interface::new_rpc_client; +use mullvad_management_interface::MullvadProxyClient; use talpid_types::ErrorExt; /// Checks if there is another instance of the daemon running. @@ -6,7 +6,7 @@ use talpid_types::ErrorExt; /// Tries to connect to another daemon and perform a simple RPC call. If it fails, assumes the /// other daemon has stopped. pub async fn is_another_instance_running() -> bool { - match new_rpc_client().await { + match MullvadProxyClient::new().await { Ok(_) => true, Err(error) => { let msg = diff --git a/mullvad-jni/src/problem_report.rs b/mullvad-jni/src/problem_report.rs index f1a7e884db..9943ec4c59 100644 --- a/mullvad-jni/src/problem_report.rs +++ b/mullvad-jni/src/problem_report.rs @@ -23,7 +23,7 @@ pub extern "system" fn Java_net_mullvad_mullvadvpn_dataproxy_MullvadProblemRepor let output_path_string = String::from_java(&env, outputPath); let output_path = Path::new(&output_path_string); - match mullvad_problem_report::collect_report(&[], output_path, Vec::new(), log_dir) { + match mullvad_problem_report::collect_report::<&str>(&[], output_path, Vec::new(), log_dir) { Ok(()) => JNI_TRUE, Err(error) => { log::error!( diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs new file mode 100644 index 0000000000..b15f076c46 --- /dev/null +++ b/mullvad-management-interface/src/client.rs @@ -0,0 +1,552 @@ +//! Client that returns and takes mullvad types as arguments instead of prost-generated types + +use crate::types; +use futures::{Stream, StreamExt}; +use mullvad_types::{ + account::{AccountData, AccountToken, VoucherSubmission}, + device::{Device, DeviceEvent, DeviceId, DeviceState, RemoveDeviceEvent}, + location::GeoIpLocation, + relay_constraints::{BridgeSettings, BridgeState, ObfuscationSettings, RelaySettingsUpdate}, + relay_list::RelayList, + settings::{DnsOptions, Settings}, + states::TunnelState, + version::AppVersionInfo, + wireguard::{PublicKey, QuantumResistantState, RotationInterval}, +}; +#[cfg(target_os = "windows")] +use std::path::Path; +#[cfg(target_os = "windows")] +use talpid_types::split_tunnel::ExcludedProcess; +use tonic::{Code, Status}; + +type Error = super::Error; + +pub type Result<T> = std::result::Result<T, super::Error>; + +#[derive(Debug, Clone)] +pub struct MullvadProxyClient(crate::ManagementServiceClient); + +pub enum DaemonEvent { + TunnelState(TunnelState), + Settings(Settings), + RelayList(RelayList), + AppVersionInfo(AppVersionInfo), + Device(DeviceEvent), + RemoveDevice(RemoveDeviceEvent), +} + +impl TryFrom<types::daemon_event::Event> for DaemonEvent { + type Error = Error; + + fn try_from(value: types::daemon_event::Event) -> Result<Self> { + match value { + types::daemon_event::Event::TunnelState(state) => TunnelState::try_from(state) + .map(DaemonEvent::TunnelState) + .map_err(Error::InvalidResponse), + types::daemon_event::Event::Settings(settings) => Settings::try_from(settings) + .map(DaemonEvent::Settings) + .map_err(Error::InvalidResponse), + types::daemon_event::Event::RelayList(list) => RelayList::try_from(list) + .map(DaemonEvent::RelayList) + .map_err(Error::InvalidResponse), + types::daemon_event::Event::VersionInfo(info) => { + Ok(DaemonEvent::AppVersionInfo(AppVersionInfo::from(info))) + } + types::daemon_event::Event::Device(event) => DeviceEvent::try_from(event) + .map(DaemonEvent::Device) + .map_err(Error::InvalidResponse), + types::daemon_event::Event::RemoveDevice(event) => RemoveDeviceEvent::try_from(event) + .map(DaemonEvent::RemoveDevice) + .map_err(Error::InvalidResponse), + } + } +} + +impl MullvadProxyClient { + pub async fn new() -> Result<Self> { + #[allow(deprecated)] + super::new_rpc_client().await.map(Self) + } + + pub async fn connect_tunnel(&mut self) -> Result<bool> { + Ok(self + .0 + .connect_tunnel(()) + .await + .map_err(Error::Rpc)? + .into_inner()) + } + + pub async fn disconnect_tunnel(&mut self) -> Result<bool> { + Ok(self + .0 + .disconnect_tunnel(()) + .await + .map_err(Error::Rpc)? + .into_inner()) + } + + pub async fn reconnect_tunnel(&mut self) -> Result<bool> { + Ok(self + .0 + .reconnect_tunnel(()) + .await + .map_err(Error::Rpc)? + .into_inner()) + } + + pub async fn get_tunnel_state(&mut self) -> Result<TunnelState> { + let state = self + .0 + .get_tunnel_state(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + TunnelState::try_from(state).map_err(Error::InvalidResponse) + } + + pub async fn events_listen(&mut self) -> Result<impl Stream<Item = Result<DaemonEvent>>> { + let listener = self + .0 + .events_listen(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + + Ok(listener.map(|item| { + let event = item + .map_err(Error::Rpc)? + .event + .ok_or(Error::MissingDaemonEvent)?; + DaemonEvent::try_from(event) + })) + } + + pub async fn prepare_restart(&mut self) -> Result<()> { + self.0.prepare_restart(()).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn factory_reset(&mut self) -> Result<()> { + self.0.factory_reset(()).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn get_current_version(&mut self) -> Result<String> { + Ok(self + .0 + .get_current_version(()) + .await + .map_err(Error::Rpc)? + .into_inner()) + } + + pub async fn get_version_info(&mut self) -> Result<AppVersionInfo> { + let version_info = self + .0 + .get_version_info(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + Ok(AppVersionInfo::from(version_info)) + } + + pub async fn get_relay_locations(&mut self) -> Result<RelayList> { + let list = self + .0 + .get_relay_locations(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + mullvad_types::relay_list::RelayList::try_from(list).map_err(Error::InvalidResponse) + } + + pub async fn update_relay_locations(&mut self) -> Result<()> { + self.0 + .update_relay_locations(()) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn update_relay_settings(&mut self, update: RelaySettingsUpdate) -> Result<()> { + let update = types::RelaySettingsUpdate::from(update); + self.0 + .update_relay_settings(update) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn get_current_location(&mut self) -> Result<GeoIpLocation> { + let location = self + .0 + .get_current_location(()) + .await + .map_err(map_location_error)? + .into_inner(); + GeoIpLocation::try_from(location).map_err(Error::InvalidResponse) + } + + pub async fn set_bridge_settings(&mut self, settings: BridgeSettings) -> Result<()> { + let settings = types::BridgeSettings::from(settings); + self.0 + .set_bridge_settings(settings) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_bridge_state(&mut self, state: BridgeState) -> Result<()> { + let state = types::BridgeState::from(state); + self.0.set_bridge_state(state).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_obfuscation_settings(&mut self, settings: ObfuscationSettings) -> Result<()> { + let settings = types::ObfuscationSettings::from(&settings); + self.0 + .set_obfuscation_settings(settings) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn get_settings(&mut self) -> Result<Settings> { + let settings = self + .0 + .get_settings(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + Settings::try_from(settings).map_err(Error::InvalidResponse) + } + + pub async fn set_allow_lan(&mut self, state: bool) -> Result<()> { + self.0.set_allow_lan(state).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_show_beta_releases(&mut self, state: bool) -> Result<()> { + self.0 + .set_show_beta_releases(state) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_block_when_disconnected(&mut self, state: bool) -> Result<()> { + self.0 + .set_block_when_disconnected(state) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_auto_connect(&mut self, state: bool) -> Result<()> { + self.0.set_auto_connect(state).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_openvpn_mssfix(&mut self, mssfix: Option<u16>) -> Result<()> { + self.0 + .set_openvpn_mssfix(mssfix.map(u32::from).unwrap_or(0)) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_wireguard_mtu(&mut self, mtu: Option<u16>) -> Result<()> { + self.0 + .set_wireguard_mtu(mtu.map(u32::from).unwrap_or(0)) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_enable_ipv6(&mut self, state: bool) -> Result<()> { + self.0.set_enable_ipv6(state).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_quantum_resistant_tunnel( + &mut self, + state: QuantumResistantState, + ) -> Result<()> { + let state = types::QuantumResistantState::from(state); + self.0 + .set_quantum_resistant_tunnel(state) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn set_dns_options(&mut self, options: DnsOptions) -> Result<()> { + let options = types::DnsOptions::from(&options); + self.0.set_dns_options(options).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn create_new_account(&mut self) -> Result<AccountToken> { + Ok(self + .0 + .create_new_account(()) + .await + .map_err(map_device_error)? + .into_inner()) + } + + pub async fn login_account(&mut self, account: AccountToken) -> Result<()> { + self.0 + .login_account(account) + .await + .map_err(map_device_error)?; + Ok(()) + } + + pub async fn logout_account(&mut self) -> Result<()> { + self.0.logout_account(()).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn get_account_data(&mut self, account: AccountToken) -> Result<AccountData> { + let data = self + .0 + .get_account_data(account) + .await + .map_err(Error::Rpc)? + .into_inner(); + AccountData::try_from(data).map_err(Error::InvalidResponse) + } + + pub async fn get_account_history(&mut self) -> Result<Option<AccountToken>> { + let history = self + .0 + .get_account_history(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + Ok(history.token) + } + + pub async fn clear_account_history(&mut self) -> Result<()> { + self.0.clear_account_history(()).await.map_err(Error::Rpc)?; + Ok(()) + } + + // get_www_auth_token + + pub async fn submit_voucher(&mut self, voucher: String) -> Result<VoucherSubmission> { + let result = self + .0 + .submit_voucher(voucher) + .await + .map_err(|error| match error.code() { + Code::NotFound => Error::InvalidVoucher, + Code::ResourceExhausted => Error::UsedVoucher, + _other => Error::Rpc(error), + })? + .into_inner(); + VoucherSubmission::try_from(result).map_err(Error::InvalidResponse) + } + + pub async fn get_device(&mut self) -> Result<DeviceState> { + let state = self + .0 + .get_device(()) + .await + .map_err(map_device_error)? + .into_inner(); + DeviceState::try_from(state).map_err(Error::InvalidResponse) + } + + pub async fn update_device(&mut self) -> Result<()> { + self.0.update_device(()).await.map_err(map_device_error)?; + Ok(()) + } + + pub async fn list_devices(&mut self, account: AccountToken) -> Result<Vec<Device>> { + let list = self + .0 + .list_devices(account) + .await + .map_err(map_device_error)? + .into_inner(); + list.devices + .into_iter() + .map(|d| Device::try_from(d).map_err(Error::InvalidResponse)) + .collect::<Result<_>>() + } + + pub async fn remove_device( + &mut self, + account: AccountToken, + device_id: DeviceId, + ) -> Result<()> { + self.0 + .remove_device(types::DeviceRemoval { + account_token: account, + device_id, + }) + .await + .map_err(map_device_error)?; + Ok(()) + } + + pub async fn set_wireguard_rotation_interval( + &mut self, + interval: RotationInterval, + ) -> Result<()> { + let duration = types::Duration::try_from(*interval.as_duration()) + .map_err(|_| Error::DurationTooLarge)?; + self.0 + .set_wireguard_rotation_interval(duration) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn reset_wireguard_rotation_interval(&mut self) -> Result<()> { + self.0 + .reset_wireguard_rotation_interval(()) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn rotate_wireguard_key(&mut self) -> Result<()> { + self.0.rotate_wireguard_key(()).await.map_err(Error::Rpc)?; + Ok(()) + } + + pub async fn get_wireguard_key(&mut self) -> Result<PublicKey> { + let key = self + .0 + .get_wireguard_key(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + PublicKey::try_from(key).map_err(Error::InvalidResponse) + } + + #[cfg(target_os = "linux")] + pub async fn get_split_tunnel_processes(&mut self) -> Result<Vec<i32>> { + use futures::TryStreamExt; + + let procs = self + .0 + .get_split_tunnel_processes(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + procs.try_collect().await.map_err(Error::Rpc) + } + + #[cfg(target_os = "linux")] + pub async fn add_split_tunnel_process(&mut self, pid: i32) -> Result<()> { + self.0 + .add_split_tunnel_process(pid) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(target_os = "linux")] + pub async fn remove_split_tunnel_process(&mut self, pid: i32) -> Result<()> { + self.0 + .remove_split_tunnel_process(pid) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(target_os = "linux")] + pub async fn clear_split_tunnel_processes(&mut self) -> Result<()> { + self.0 + .clear_split_tunnel_processes(()) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(target_os = "windows")] + pub async fn add_split_tunnel_app<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { + let path = path.as_ref().to_str().ok_or(Error::PathMustBeUtf8)?; + self.0 + .add_split_tunnel_app(path.to_owned()) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(target_os = "windows")] + pub async fn remove_split_tunnel_app<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { + let path = path.as_ref().to_str().ok_or(Error::PathMustBeUtf8)?; + self.0 + .remove_split_tunnel_app(path.to_owned()) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(target_os = "windows")] + pub async fn clear_split_tunnel_apps(&mut self) -> Result<()> { + self.0 + .clear_split_tunnel_apps(()) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(target_os = "windows")] + pub async fn set_split_tunnel_state(&mut self, state: bool) -> Result<()> { + self.0 + .set_split_tunnel_state(state) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + #[cfg(target_os = "windows")] + pub async fn get_excluded_processes(&mut self) -> Result<Vec<ExcludedProcess>> { + let procs = self + .0 + .get_excluded_processes(()) + .await + .map_err(Error::Rpc)? + .into_inner(); + Ok(procs + .processes + .into_iter() + .map(ExcludedProcess::from) + .collect::<Vec<_>>()) + } + + #[cfg(target_os = "windows")] + pub async fn set_use_wireguard_nt(&mut self, state: bool) -> Result<()> { + self.0 + .set_use_wireguard_nt(state) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + + // check_volumes +} + +fn map_device_error(status: Status) -> Error { + match status.code() { + Code::ResourceExhausted => Error::TooManyDevices, + Code::Unauthenticated => Error::InvalidAccount, + Code::AlreadyExists => Error::AlreadyLoggedIn, + Code::NotFound => Error::DeviceNotFound, + _other => Error::Rpc(status), + } +} + +fn map_location_error(status: Status) -> Error { + match status.code() { + Code::NotFound => Error::NoLocationData, + _other => Error::Rpc(status), + } +} diff --git a/mullvad-management-interface/src/lib.rs b/mullvad-management-interface/src/lib.rs index c5b4d80487..002d122435 100644 --- a/mullvad-management-interface/src/lib.rs +++ b/mullvad-management-interface/src/lib.rs @@ -1,3 +1,4 @@ +pub mod client; pub mod types; use parity_tokio_ipc::Endpoint as IpcEndpoint; @@ -51,8 +52,45 @@ pub enum Error { #[cfg(unix)] #[error(display = "Failed to set group ID")] SetGidError(#[error(source)] nix::Error), + + #[error(display = "gRPC call returned error")] + Rpc(#[error(source)] tonic::Status), + + #[error(display = "Failed to parse gRPC response")] + InvalidResponse(#[error(source)] types::FromProtobufTypeError), + + #[error(display = "Duration is too large")] + DurationTooLarge, + + #[error(display = "Unexpected non-UTF8 string")] + PathMustBeUtf8, + + #[error(display = "Missing daemon event")] + MissingDaemonEvent, + + #[error(display = "This voucher code is invalid")] + InvalidVoucher, + + #[error(display = "This voucher code has already been used")] + UsedVoucher, + + #[error(display = "There are too many devices on the account. One must be revoked to log in")] + TooManyDevices, + + #[error(display = "You are already logged in. Log out to create a new account")] + AlreadyLoggedIn, + + #[error(display = "The account does not exist")] + InvalidAccount, + + #[error(display = "There is no such device")] + DeviceNotFound, + + #[error(display = "Location data is unavailable")] + NoLocationData, } +#[deprecated(note = "Prefer MullvadProxyClient")] pub async fn new_rpc_client() -> Result<ManagementServiceClient, Error> { let ipc_path = mullvad_paths::get_rpc_socket_path(); @@ -67,6 +105,8 @@ pub async fn new_rpc_client() -> Result<ManagementServiceClient, Error> { Ok(ManagementServiceClient::new(channel)) } +pub use client::MullvadProxyClient; + pub type ServerJoinHandle = tokio::task::JoinHandle<Result<(), Error>>; pub async fn spawn_rpc_server<T: ManagementService, F: Future<Output = ()> + Send + 'static>( diff --git a/mullvad-management-interface/src/types/conversions/account.rs b/mullvad-management-interface/src/types/conversions/account.rs new file mode 100644 index 0000000000..40c3e797be --- /dev/null +++ b/mullvad-management-interface/src/types/conversions/account.rs @@ -0,0 +1,59 @@ +use crate::types; +use mullvad_types::account::{AccountData, VoucherSubmission}; + +use super::FromProtobufTypeError; + +impl From<VoucherSubmission> for types::VoucherSubmission { + fn from(submission: VoucherSubmission) -> Self { + types::VoucherSubmission { + seconds_added: submission.time_added, + new_expiry: Some(types::Timestamp { + seconds: submission.new_expiry.timestamp(), + nanos: 0, + }), + } + } +} + +impl TryFrom<types::VoucherSubmission> for VoucherSubmission { + type Error = FromProtobufTypeError; + + fn try_from(submission: types::VoucherSubmission) -> Result<Self, FromProtobufTypeError> { + let new_expiry = submission + .new_expiry + .ok_or(FromProtobufTypeError::InvalidArgument("missing expiry"))?; + let ndt = + chrono::NaiveDateTime::from_timestamp(new_expiry.seconds, new_expiry.nanos as u32); + + Ok(VoucherSubmission { + new_expiry: chrono::DateTime::<chrono::Utc>::from_utc(ndt, chrono::Utc), + time_added: submission.seconds_added, + }) + } +} + +impl From<AccountData> for types::AccountData { + fn from(data: AccountData) -> Self { + types::AccountData { + expiry: Some(types::Timestamp { + seconds: data.expiry.timestamp(), + nanos: 0, + }), + } + } +} + +impl TryFrom<types::AccountData> for AccountData { + type Error = FromProtobufTypeError; + + fn try_from(data: types::AccountData) -> Result<Self, FromProtobufTypeError> { + let expiry = data + .expiry + .ok_or(FromProtobufTypeError::InvalidArgument("missing expiry"))?; + let ndt = chrono::NaiveDateTime::from_timestamp(expiry.seconds, expiry.nanos as u32); + + Ok(AccountData { + expiry: chrono::DateTime::<chrono::Utc>::from_utc(ndt, chrono::Utc), + }) + } +} diff --git a/mullvad-management-interface/src/types/conversions/device.rs b/mullvad-management-interface/src/types/conversions/device.rs index 625af6c9c1..cd1aeb5e4f 100644 --- a/mullvad-management-interface/src/types/conversions/device.rs +++ b/mullvad-management-interface/src/types/conversions/device.rs @@ -51,6 +51,40 @@ impl From<mullvad_types::device::Device> for proto::Device { } } +impl TryFrom<proto::DeviceState> for mullvad_types::device::DeviceState { + type Error = FromProtobufTypeError; + + fn try_from(state: proto::DeviceState) -> Result<Self, FromProtobufTypeError> { + let state_type = proto::device_state::State::from_i32(state.state).ok_or( + FromProtobufTypeError::InvalidArgument("invalid device state"), + )?; + + match state_type { + proto::device_state::State::LoggedIn => { + let account = state.device.ok_or(FromProtobufTypeError::InvalidArgument( + "missing account data", + ))?; + let device = account + .device + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing device data", + ))?; + + Ok(mullvad_types::device::DeviceState::LoggedIn( + mullvad_types::device::AccountAndDevice { + account_token: account.account_token, + device: mullvad_types::device::Device::try_from(device)?, + }, + )) + } + proto::device_state::State::Revoked => Ok(mullvad_types::device::DeviceState::Revoked), + proto::device_state::State::LoggedOut => { + Ok(mullvad_types::device::DeviceState::LoggedOut) + } + } + } +} + impl From<mullvad_types::device::DevicePort> for proto::DevicePort { fn from(port: mullvad_types::device::DevicePort) -> Self { proto::DevicePort { id: port.id } @@ -83,12 +117,28 @@ impl From<&mullvad_types::device::DeviceState> for proto::device_state::State { impl From<mullvad_types::device::DeviceEvent> for proto::DeviceEvent { fn from(event: mullvad_types::device::DeviceEvent) -> Self { proto::DeviceEvent { - cause: proto::device_event::Cause::from(event.cause) as i32, + cause: i32::from(proto::device_event::Cause::from(event.cause)), new_state: Some(proto::DeviceState::from(event.new_state)), } } } +impl TryFrom<proto::DeviceEvent> for mullvad_types::device::DeviceEvent { + type Error = FromProtobufTypeError; + + fn try_from(event: proto::DeviceEvent) -> Result<Self, Self::Error> { + let cause = proto::device_event::Cause::from_i32(event.cause) + .ok_or(FromProtobufTypeError::InvalidArgument("invalid event"))?; + let cause = mullvad_types::device::DeviceEventCause::from(cause); + + let new_state = mullvad_types::device::DeviceState::try_from(event.new_state.ok_or( + FromProtobufTypeError::InvalidArgument("missing device state"), + )?)?; + + Ok(mullvad_types::device::DeviceEvent { cause, new_state }) + } +} + impl From<mullvad_types::device::DeviceEventCause> for proto::device_event::Cause { fn from(cause: mullvad_types::device::DeviceEventCause) -> Self { use mullvad_types::device::DeviceEventCause as MullvadEvent; @@ -102,6 +152,19 @@ impl From<mullvad_types::device::DeviceEventCause> for proto::device_event::Caus } } +impl From<proto::device_event::Cause> for mullvad_types::device::DeviceEventCause { + fn from(event: proto::device_event::Cause) -> Self { + use mullvad_types::device::DeviceEventCause as MullvadEvent; + match event { + proto::device_event::Cause::LoggedIn => MullvadEvent::LoggedIn, + proto::device_event::Cause::LoggedOut => MullvadEvent::LoggedOut, + proto::device_event::Cause::Revoked => MullvadEvent::Revoked, + proto::device_event::Cause::Updated => MullvadEvent::Updated, + proto::device_event::Cause::RotatedKey => MullvadEvent::RotatedKey, + } + } +} + impl From<mullvad_types::device::RemoveDeviceEvent> for proto::RemoveDeviceEvent { fn from(event: mullvad_types::device::RemoveDeviceEvent) -> Self { proto::RemoveDeviceEvent { @@ -115,6 +178,22 @@ impl From<mullvad_types::device::RemoveDeviceEvent> for proto::RemoveDeviceEvent } } +impl TryFrom<proto::RemoveDeviceEvent> for mullvad_types::device::RemoveDeviceEvent { + type Error = FromProtobufTypeError; + + fn try_from(event: proto::RemoveDeviceEvent) -> Result<Self, Self::Error> { + let new_devices = event + .new_device_list + .into_iter() + .map(mullvad_types::device::Device::try_from) + .collect::<Result<Vec<_>, FromProtobufTypeError>>()?; + Ok(mullvad_types::device::RemoveDeviceEvent { + account_token: event.account_token, + new_devices, + }) + } +} + impl From<mullvad_types::device::AccountAndDevice> for proto::AccountAndDevice { fn from(device: mullvad_types::device::AccountAndDevice) -> Self { proto::AccountAndDevice { diff --git a/mullvad-management-interface/src/types/conversions/mod.rs b/mullvad-management-interface/src/types/conversions/mod.rs index 46639424fd..51c53ef76b 100644 --- a/mullvad-management-interface/src/types/conversions/mod.rs +++ b/mullvad-management-interface/src/types/conversions/mod.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +mod account; mod custom_tunnel; mod device; mod location; @@ -7,12 +8,15 @@ mod net; pub mod relay_constraints; mod relay_list; mod settings; +#[cfg(target_os = "windows")] +mod split_tunnel; mod states; mod version; mod wireguard; -#[derive(Debug)] +#[derive(err_derive::Error, Debug)] pub enum FromProtobufTypeError { + #[error(display = "Invalid argument for type conversion: {}", _0)] InvalidArgument(&'static str), } diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs index 23f628ad9b..3254123929 100644 --- a/mullvad-management-interface/src/types/conversions/relay_list.rs +++ b/mullvad-management-interface/src/types/conversions/relay_list.rs @@ -1,8 +1,15 @@ +use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + use crate::types::{ conversions::{bytes_to_pubkey, option_from_proto_string, to_proto_any, try_from_proto_any}, proto, FromProtobufTypeError, }; +use super::net::try_transport_protocol_from_i32; + impl From<mullvad_types::relay_list::RelayList> for proto::RelayList { fn from(relay_list: mullvad_types::relay_list::RelayList) -> Self { let mut proto_list = proto::RelayList { @@ -134,6 +141,76 @@ impl From<mullvad_types::relay_list::Relay> for proto::Relay { } } +impl TryFrom<proto::RelayList> for mullvad_types::relay_list::RelayList { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::RelayList) -> Result<Self, Self::Error> { + let wireguard = value + .wireguard + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing wireguard data", + ))?; + let bridge = value.bridge.ok_or(FromProtobufTypeError::InvalidArgument( + "missing bridge data", + ))?; + let openvpn = value.openvpn.ok_or(FromProtobufTypeError::InvalidArgument( + "missing openvpn data", + ))?; + + let countries = value + .countries + .into_iter() + .map(mullvad_types::relay_list::RelayListCountry::try_from) + .collect::<Result<Vec<_>, _>>()?; + + Ok(mullvad_types::relay_list::RelayList { + etag: None, + countries, + openvpn: mullvad_types::relay_list::OpenVpnEndpointData::try_from(openvpn)?, + bridge: mullvad_types::relay_list::BridgeEndpointData::try_from(bridge)?, + wireguard: mullvad_types::relay_list::WireguardEndpointData::try_from(wireguard)?, + }) + } +} + +impl TryFrom<proto::RelayListCountry> for mullvad_types::relay_list::RelayListCountry { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::RelayListCountry) -> Result<Self, Self::Error> { + let cities = value + .cities + .into_iter() + .map(mullvad_types::relay_list::RelayListCity::try_from) + .collect::<Result<Vec<_>, _>>()?; + + Ok(mullvad_types::relay_list::RelayListCountry { + cities, + code: value.code, + name: value.name, + }) + } +} + +impl TryFrom<proto::RelayListCity> for mullvad_types::relay_list::RelayListCity { + type Error = FromProtobufTypeError; + + fn try_from(value: proto::RelayListCity) -> Result<Self, Self::Error> { + let relays = value + .relays + .into_iter() + .map(mullvad_types::relay_list::Relay::try_from) + .collect::<Result<Vec<_>, _>>()?; + + Ok(mullvad_types::relay_list::RelayListCity { + code: value.code, + latitude: value.latitude, + longitude: value.longitude, + name: value.name, + relays, + }) + } +} + impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { type Error = FromProtobufTypeError; @@ -203,3 +280,100 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay { }) } } + +impl TryFrom<proto::OpenVpnEndpointData> for mullvad_types::relay_list::OpenVpnEndpointData { + type Error = FromProtobufTypeError; + + fn try_from(openvpn: proto::OpenVpnEndpointData) -> Result<Self, FromProtobufTypeError> { + let ports = openvpn + .endpoints + .into_iter() + .map(mullvad_types::relay_list::OpenVpnEndpoint::try_from) + .collect::<Result<Vec<_>, FromProtobufTypeError>>()?; + + Ok(mullvad_types::relay_list::OpenVpnEndpointData { ports }) + } +} + +impl TryFrom<proto::OpenVpnEndpoint> for mullvad_types::relay_list::OpenVpnEndpoint { + type Error = FromProtobufTypeError; + + fn try_from(openvpn: proto::OpenVpnEndpoint) -> Result<Self, FromProtobufTypeError> { + Ok(mullvad_types::relay_list::OpenVpnEndpoint { + port: u16::try_from(openvpn.port) + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid port"))?, + protocol: try_transport_protocol_from_i32(openvpn.protocol)?, + }) + } +} + +impl TryFrom<proto::BridgeEndpointData> for mullvad_types::relay_list::BridgeEndpointData { + type Error = FromProtobufTypeError; + + fn try_from(bridge: proto::BridgeEndpointData) -> Result<Self, FromProtobufTypeError> { + let shadowsocks = bridge + .shadowsocks + .into_iter() + .map(mullvad_types::relay_list::ShadowsocksEndpointData::try_from) + .collect::<Result<Vec<_>, FromProtobufTypeError>>()?; + + Ok(mullvad_types::relay_list::BridgeEndpointData { shadowsocks }) + } +} + +impl TryFrom<proto::ShadowsocksEndpointData> + for mullvad_types::relay_list::ShadowsocksEndpointData +{ + type Error = FromProtobufTypeError; + + fn try_from( + shadowsocks: proto::ShadowsocksEndpointData, + ) -> Result<Self, FromProtobufTypeError> { + Ok(mullvad_types::relay_list::ShadowsocksEndpointData { + port: u16::try_from(shadowsocks.port) + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid port"))?, + cipher: shadowsocks.cipher, + password: shadowsocks.password, + protocol: try_transport_protocol_from_i32(shadowsocks.protocol)?, + }) + } +} + +impl TryFrom<proto::WireguardEndpointData> for mullvad_types::relay_list::WireguardEndpointData { + type Error = FromProtobufTypeError; + + fn try_from(wireguard: proto::WireguardEndpointData) -> Result<Self, FromProtobufTypeError> { + let port_ranges = wireguard + .port_ranges + .into_iter() + .map(|range| { + let first = u16::try_from(range.first) + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid wg port"))?; + let last = u16::try_from(range.last) + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid wg port"))?; + Ok((first, last)) + }) + .collect::<Result<Vec<(u16, u16)>, FromProtobufTypeError>>()?; + + let ipv4_gateway = Ipv4Addr::from_str(&wireguard.ipv4_gateway) + .map_err(|_| FromProtobufTypeError::InvalidArgument("Invalid IPv4 gateway"))?; + let ipv6_gateway = Ipv6Addr::from_str(&wireguard.ipv6_gateway) + .map_err(|_| FromProtobufTypeError::InvalidArgument("Invalid IPv6 gateway"))?; + + let udp2tcp_ports = wireguard + .udp2tcp_ports + .into_iter() + .map(|port| { + u16::try_from(port) + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid udp2tcp port")) + }) + .collect::<Result<Vec<u16>, FromProtobufTypeError>>()?; + + Ok(mullvad_types::relay_list::WireguardEndpointData { + port_ranges, + ipv4_gateway, + ipv6_gateway, + udp2tcp_ports, + }) + } +} diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs index 39ff2c05a2..73007f32f9 100644 --- a/mullvad-management-interface/src/types/conversions/settings.rs +++ b/mullvad-management-interface/src/types/conversions/settings.rs @@ -1,4 +1,5 @@ use crate::types::{proto, FromProtobufTypeError}; +use mullvad_types::settings::CURRENT_SETTINGS_VERSION; use talpid_types::ErrorExt; impl From<&mullvad_types::settings::Settings> for proto::Settings { @@ -99,6 +100,106 @@ impl From<&mullvad_types::settings::TunnelOptions> for proto::TunnelOptions { } } +impl TryFrom<proto::Settings> for mullvad_types::settings::Settings { + type Error = FromProtobufTypeError; + + fn try_from(settings: proto::Settings) -> Result<Self, Self::Error> { + let relay_settings = + settings + .relay_settings + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing relay settings", + ))?; + let bridge_settings = + settings + .bridge_settings + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing bridge settings", + ))?; + let bridge_state = settings + .bridge_state + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing bridge state", + )) + .and_then(|state| try_bridge_state_from_i32(state.state))?; + let tunnel_options = + settings + .tunnel_options + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing tunnel options", + ))?; + let obfuscation_settings = + settings + .obfuscation_settings + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing obfuscation settings", + ))?; + #[cfg(windows)] + let split_tunnel = settings + .split_tunnel + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing split tunnel options", + ))?; + + Ok(Self { + relay_settings: mullvad_types::relay_constraints::RelaySettings::try_from( + relay_settings, + )?, + bridge_settings: mullvad_types::relay_constraints::BridgeSettings::try_from( + bridge_settings, + )?, + bridge_state, + allow_lan: settings.allow_lan, + block_when_disconnected: settings.block_when_disconnected, + auto_connect: settings.auto_connect, + tunnel_options: mullvad_types::settings::TunnelOptions::try_from(tunnel_options)?, + show_beta_releases: settings.show_beta_releases, + #[cfg(windows)] + split_tunnel: mullvad_types::settings::SplitTunnelSettings::from(split_tunnel), + obfuscation_settings: mullvad_types::relay_constraints::ObfuscationSettings::try_from( + obfuscation_settings, + )?, + // NOTE: This field is meaningless when obtained from gRPC + wg_migration_rand_num: std::f32::NAN, + // NOTE: This field is set based on mullvad-types. It's not based on the actual settings version. + settings_version: CURRENT_SETTINGS_VERSION, + }) + } +} + +pub fn try_bridge_state_from_i32( + bridge_state: i32, +) -> Result<mullvad_types::relay_constraints::BridgeState, FromProtobufTypeError> { + match proto::bridge_state::State::from_i32(bridge_state) { + Some(proto::bridge_state::State::Auto) => { + Ok(mullvad_types::relay_constraints::BridgeState::Auto) + } + Some(proto::bridge_state::State::On) => { + Ok(mullvad_types::relay_constraints::BridgeState::On) + } + Some(proto::bridge_state::State::Off) => { + Ok(mullvad_types::relay_constraints::BridgeState::Off) + } + None => Err(FromProtobufTypeError::InvalidArgument( + "invalid bridge state", + )), + } +} + +#[cfg(windows)] +impl From<proto::SplitTunnelSettings> for mullvad_types::settings::SplitTunnelSettings { + fn from(value: proto::SplitTunnelSettings) -> Self { + mullvad_types::settings::SplitTunnelSettings { + enable_exclusions: value.enable_exclusions, + apps: value + .apps + .into_iter() + .map(std::path::PathBuf::from) + .collect(), + } + } +} + impl TryFrom<proto::TunnelOptions> for mullvad_types::settings::TunnelOptions { type Error = FromProtobufTypeError; diff --git a/mullvad-management-interface/src/types/conversions/split_tunnel.rs b/mullvad-management-interface/src/types/conversions/split_tunnel.rs new file mode 100644 index 0000000000..7c307cdea4 --- /dev/null +++ b/mullvad-management-interface/src/types/conversions/split_tunnel.rs @@ -0,0 +1,23 @@ +use crate::types; +use std::path::PathBuf; +use talpid_types::split_tunnel::ExcludedProcess; + +impl From<ExcludedProcess> for types::ExcludedProcess { + fn from(value: ExcludedProcess) -> Self { + types::ExcludedProcess { + image: value.image.to_string_lossy().into_owned(), + inherited: value.inherited, + pid: value.pid, + } + } +} + +impl From<types::ExcludedProcess> for ExcludedProcess { + fn from(value: types::ExcludedProcess) -> Self { + ExcludedProcess { + image: PathBuf::from(value.image), + inherited: value.inherited, + pid: value.pid, + } + } +} diff --git a/mullvad-management-interface/src/types/conversions/version.rs b/mullvad-management-interface/src/types/conversions/version.rs index 0a21667695..7bb80465fa 100644 --- a/mullvad-management-interface/src/types/conversions/version.rs +++ b/mullvad-management-interface/src/types/conversions/version.rs @@ -10,3 +10,20 @@ impl From<mullvad_types::version::AppVersionInfo> for proto::AppVersionInfo { } } } + +impl From<proto::AppVersionInfo> for mullvad_types::version::AppVersionInfo { + fn from(version_info: proto::AppVersionInfo) -> Self { + Self { + supported: version_info.supported, + latest_stable: version_info.latest_stable, + latest_beta: version_info.latest_beta, + suggested_upgrade: if version_info.suggested_upgrade.is_empty() { + None + } else { + Some(version_info.suggested_upgrade) + }, + // NOTE: This field is meaningless when derived from the gRPC type + wg_migration_threshold: f32::NAN, + } + } +} diff --git a/mullvad-management-interface/src/types/conversions/wireguard.rs b/mullvad-management-interface/src/types/conversions/wireguard.rs index ce2d4c7dba..29cb8df465 100644 --- a/mullvad-management-interface/src/types/conversions/wireguard.rs +++ b/mullvad-management-interface/src/types/conversions/wireguard.rs @@ -14,6 +14,25 @@ impl From<mullvad_types::wireguard::PublicKey> for proto::PublicKey { } } +impl TryFrom<proto::PublicKey> for mullvad_types::wireguard::PublicKey { + type Error = FromProtobufTypeError; + + fn try_from(public_key: proto::PublicKey) -> Result<Self, Self::Error> { + let created = public_key + .created + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing 'created' timestamp", + ))?; + let ndt = chrono::NaiveDateTime::from_timestamp(created.seconds, created.nanos as u32); + + Ok(mullvad_types::wireguard::PublicKey { + key: talpid_types::net::wireguard::PublicKey::try_from(public_key.key.as_slice()) + .map_err(|_| FromProtobufTypeError::InvalidArgument("invalid wireguard key"))?, + created: chrono::DateTime::<chrono::Utc>::from_utc(ndt, chrono::Utc), + }) + } +} + impl From<mullvad_types::wireguard::QuantumResistantState> for proto::QuantumResistantState { fn from(state: mullvad_types::wireguard::QuantumResistantState) -> Self { match state { diff --git a/mullvad-nsis/Cargo.toml b/mullvad-nsis/Cargo.toml index 0c83da480b..f31378f5c5 100644 --- a/mullvad-nsis/Cargo.toml +++ b/mullvad-nsis/Cargo.toml @@ -14,4 +14,4 @@ crate_type = ["staticlib"] mullvad-paths = { path = "../mullvad-paths" } [target.i686-pc-windows-msvc.build-dependencies] -cbindgen = "0.24.3" +cbindgen = { version = "0.24.3", default-features = false } diff --git a/mullvad-problem-report/Cargo.toml b/mullvad-problem-report/Cargo.toml index d516186203..5992b4570f 100644 --- a/mullvad-problem-report/Cargo.toml +++ b/mullvad-problem-report/Cargo.toml @@ -23,7 +23,7 @@ talpid-types = { path = "../talpid-types" } talpid-platform-metadata = { path = "../talpid-platform-metadata" } [target.'cfg(not(target_os="android"))'.dependencies] -clap = { version = "3.0", features = ["cargo"] } +clap = { version = "4.2.7", features = ["cargo"] } env_logger = "0.10.0" [target.'cfg(target_os = "android")'.dependencies] diff --git a/mullvad-problem-report/src/lib.rs b/mullvad-problem-report/src/lib.rs index 9a9b347677..088ceb27df 100644 --- a/mullvad-problem-report/src/lib.rs +++ b/mullvad-problem-report/src/lib.rs @@ -106,8 +106,8 @@ pub enum LogError { NoLocalAppDataDir, } -pub fn collect_report( - extra_logs: &[&Path], +pub fn collect_report<P: AsRef<Path>>( + extra_logs: &[P], output_path: &Path, redact_custom_strings: Vec<String>, #[cfg(target_os = "android")] android_log_dir: &Path, diff --git a/mullvad-problem-report/src/main.rs b/mullvad-problem-report/src/main.rs index af38f614d2..50a2e0ce18 100644 --- a/mullvad-problem-report/src/main.rs +++ b/mullvad-problem-report/src/main.rs @@ -1,8 +1,12 @@ #![deny(rust_2018_idioms)] -use clap::{crate_authors, crate_name}; +use clap::Parser; use mullvad_problem_report::{collect_report, Error}; -use std::{env, path::Path, process}; +use std::{ + env, + path::{Path, PathBuf}, + process, +}; use talpid_types::ErrorExt; fn main() { @@ -15,110 +19,70 @@ fn main() { }) } +#[derive(Debug, Parser)] +#[command(author, version = mullvad_version::VERSION, about, long_about = None)] +#[command( + arg_required_else_help = true, + disable_help_subcommand = true, + disable_version_flag = true +)] +enum Cli { + /// Collect problem report to a single file + Collect { + /// The destination path for saving the collected report + #[arg(required = true, long, short = 'o')] + output: PathBuf, + /// Paths to additional log files to be included + extra_logs: Vec<PathBuf>, + /// List of strings to remove from the report + #[arg(long)] + redact: Vec<String>, + }, + + /// Send collected problem report + Send { + /// Path to a previously collected report file + #[arg(required = true, long, short = 'r')] + report: PathBuf, + /// Email to attach to the problem report + #[arg(long, short = 'e')] + email: Option<String>, + /// Message to include in the problem report + #[arg(long, short = 'm')] + message: Option<String>, + }, +} + fn run() -> Result<(), Error> { env_logger::init(); - let app = clap::App::new(crate_name!()) - .version(mullvad_version::VERSION) - .author(crate_authors!()) - .about("Mullvad VPN problem report tool. Collects logs and sends them to Mullvad support.") - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .global_setting(clap::AppSettings::DisableHelpSubcommand) - .global_setting(clap::AppSettings::DisableVersionFlag) - .subcommand( - clap::App::new("collect") - .about("Collect problem report") - .arg( - clap::Arg::new("output") - .help("The destination path for saving the collected report.") - .long("output") - .short('o') - .value_name("PATH") - .allow_invalid_utf8(true) - .takes_value(true) - .required(true), - ) - .arg( - clap::Arg::new("extra_logs") - .help("Paths to additional log files to be included.") - .multiple_occurrences(true) - .multiple_values(true) - .value_name("EXTRA LOGS") - .allow_invalid_utf8(true) - .takes_value(true) - .required(false), - ) - .arg( - clap::Arg::new("redact") - .help("List of words and expressions to remove from the report") - .long("redact") - .value_name("PHRASE") - .multiple_occurrences(true) - .multiple_values(true) - .takes_value(true), - ), - ) - .subcommand( - clap::App::new("send") - .about("Send collected problem report") - .arg( - clap::Arg::new("report") - .long("report") - .short('r') - .help("The path to previously collected report file.") - .allow_invalid_utf8(true) - .takes_value(true) - .required(true), - ) - .arg( - clap::Arg::new("email") - .long("email") - .short('e') - .help("Reporter's email") - .takes_value(true) - .required(false), - ) - .arg( - clap::Arg::new("message") - .long("message") - .short('m') - .help("Reporter's message") - .takes_value(true) - .required(false), - ), - ); - - let matches = app.get_matches(); - if let Some(collect_matches) = matches.subcommand_matches("collect") { - let redact_custom_strings = collect_matches - .values_of_t("redact") - .unwrap_or_else(|_| vec![]); - let extra_logs = collect_matches - .values_of_os("extra_logs") - .map(|os_values| os_values.map(Path::new).collect()) - .unwrap_or_else(Vec::new); - let output_path = Path::new(collect_matches.value_of_os("output").unwrap()); - collect_report(&extra_logs, output_path, redact_custom_strings)?; + match Cli::parse() { + Cli::Collect { + output, + extra_logs, + redact, + } => { + collect_report(&extra_logs, &output, redact)?; - let expanded_output_path = output_path - .canonicalize() - .unwrap_or_else(|_| output_path.to_owned()); - println!( - "Problem report written to {}", - expanded_output_path.display() - ); - println!(); - println!("Send the problem report to support via the send subcommand. See:"); - println!(" $ {} send --help", env::args().next().unwrap()); - Ok(()) - } else if let Some(send_matches) = matches.subcommand_matches("send") { - let report_path = Path::new(send_matches.value_of_os("report").unwrap()); - let user_email = send_matches.value_of("email").unwrap_or(""); - let user_message = send_matches.value_of("message").unwrap_or(""); - send_problem_report(user_email, user_message, report_path) - } else { - unreachable!("No sub command given"); + println!("Problem report written to {}", output.display()); + println!(); + println!("Send the problem report to support via the send subcommand. See:"); + println!(" $ {} send --help", env::args().next().unwrap()); + } + Cli::Send { + report, + email, + message, + } => { + send_problem_report( + &email.unwrap_or_default(), + &message.unwrap_or_default(), + &report, + )?; + } } + + Ok(()) } fn send_problem_report( @@ -128,9 +92,11 @@ fn send_problem_report( ) -> Result<(), Error> { let cache_dir = mullvad_paths::get_cache_dir().map_err(Error::ObtainCacheDirectory)?; mullvad_problem_report::send_problem_report(user_email, user_message, report_path, &cache_dir) - .map(|()| println!("Problem report sent")) .map_err(|error| { eprintln!("{}", error.display_chain()); error - }) + })?; + + println!("Problem report sent"); + Ok(()) } diff --git a/mullvad-setup/Cargo.toml b/mullvad-setup/Cargo.toml index 91880b6bfc..651dac4aaf 100644 --- a/mullvad-setup/Cargo.toml +++ b/mullvad-setup/Cargo.toml @@ -12,7 +12,7 @@ name = "mullvad-setup" path = "src/main.rs" [dependencies] -clap = { version = "3.0", features = ["cargo"] } +clap = { version = "4.2.7", features = ["cargo"] } env_logger = "0.10.0" err-derive = "0.3.1" lazy_static = "1.1.0" diff --git a/mullvad-setup/src/main.rs b/mullvad-setup/src/main.rs index 3dcf2f9287..8cf6ad3778 100644 --- a/mullvad-setup/src/main.rs +++ b/mullvad-setup/src/main.rs @@ -1,6 +1,6 @@ -use clap::{crate_authors, crate_description, crate_name, App}; +use clap::Parser; use mullvad_api::{self, proxy::ApiConnectionMode}; -use mullvad_management_interface::new_rpc_client; +use mullvad_management_interface::MullvadProxyClient; use mullvad_types::version::ParsedAppVersion; use std::{path::PathBuf, process, str::FromStr, time::Duration}; use talpid_core::{ @@ -41,7 +41,7 @@ pub enum Error { RpcConnectionError(#[error(source)] mullvad_management_interface::Error), #[error(display = "RPC call failed")] - DaemonRpcError(#[error(source)] mullvad_management_interface::Status), + DaemonRpcError(#[error(source)] mullvad_management_interface::Error), #[error(display = "This command cannot be run if the daemon is active")] DaemonIsRunning, @@ -71,47 +71,44 @@ pub enum Error { ParseVersionStringError, } +#[derive(Debug, Parser)] +#[command(author, version = mullvad_version::VERSION, about, long_about = None)] +#[command(propagate_version = true)] +#[command( + arg_required_else_help = true, + disable_help_subcommand = true, + disable_version_flag = true +)] +enum Cli { + /// Move a running daemon into a blocking state and save its target state + PrepareRestart, + /// Remove any firewall rules introduced by the daemon + ResetFirewall, + /// Remove the current device from the active account + RemoveDevice, + /// Checks whether the given version is older than the current version + IsOlderVersion { + /// Version string to compare the current version + #[arg(required = true)] + old_version: String, + }, +} + #[tokio::main] async fn main() { env_logger::init(); - let subcommands = vec![ - App::new("prepare-restart") - .about("Move a running daemon into a blocking state and save its target state"), - App::new("reset-firewall").about("Remove any firewall rules introduced by the daemon"), - App::new("remove-device").about("Remove the current device from the active account"), - App::new("is-older-version") - .about("Checks whether the given version is older than the current version") - .arg( - clap::Arg::new("OLDVERSION") - .required(true) - .help("Version string to compare the current version"), - ), - ]; - - let app = clap::App::new(crate_name!()) - .version(mullvad_version::VERSION) - .author(crate_authors!()) - .about(crate_description!()) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .global_setting(clap::AppSettings::DisableHelpSubcommand) - .global_setting(clap::AppSettings::DisableVersionFlag) - .subcommands(subcommands); - - let matches = app.get_matches(); - let result = match matches.subcommand() { - Some(("prepare-restart", _)) => prepare_restart().await, - Some(("reset-firewall", _)) => reset_firewall().await, - Some(("remove-device", _)) => remove_device().await, - Some(("is-older-version", sub_matches)) => { - let old_version = sub_matches.value_of("OLDVERSION").unwrap(); - match is_older_version(old_version) { + let result = match Cli::parse() { + Cli::PrepareRestart => prepare_restart().await, + Cli::ResetFirewall => reset_firewall().await, + Cli::RemoveDevice => remove_device().await, + Cli::IsOlderVersion { old_version } => { + match is_older_version(&old_version) { // Returning exit status Ok(status) => process::exit(status as i32), Err(error) => Err(error), } } - _ => unreachable!("No command matched"), }; if let Err(e) = result { @@ -132,16 +129,16 @@ fn is_older_version(old_version: &str) -> Result<ExitStatus, Error> { } async fn prepare_restart() -> Result<(), Error> { - let mut rpc = new_rpc_client().await.map_err(Error::RpcConnectionError)?; - rpc.prepare_restart(()) + let mut rpc = MullvadProxyClient::new() .await - .map_err(Error::DaemonRpcError)?; + .map_err(Error::RpcConnectionError)?; + rpc.prepare_restart().await.map_err(Error::DaemonRpcError)?; Ok(()) } async fn reset_firewall() -> Result<(), Error> { // Ensure that the daemon isn't running - if new_rpc_client().await.is_ok() { + if MullvadProxyClient::new().await.is_ok() { return Err(Error::DaemonIsRunning); } diff --git a/mullvad-types/Cargo.toml b/mullvad-types/Cargo.toml index afa2246a87..9f4fe7000d 100644 --- a/mullvad-types/Cargo.toml +++ b/mullvad-types/Cargo.toml @@ -19,5 +19,7 @@ rand = "0.8" talpid-types = { path = "../talpid-types" } +clap = { version = "4.2.7", features = ["derive"], optional = true } + [target.'cfg(target_os = "android")'.dependencies] jnix = { version = "0.5", features = ["derive"] } diff --git a/mullvad-types/src/device.rs b/mullvad-types/src/device.rs index 628b86b685..f21d890eca 100644 --- a/mullvad-types/src/device.rs +++ b/mullvad-types/src/device.rs @@ -82,6 +82,17 @@ impl DeviceState { _ => None, } } + + pub fn is_logged_in(&self) -> bool { + matches!(self, Self::LoggedIn(_)) + } + + pub fn get_account(&self) -> Option<&AccountAndDevice> { + match self { + DeviceState::LoggedIn(ref account) => Some(account), + _ => None, + } + } } /// A [Device] and its associated account token. diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index 79a95ff031..9d896a46c7 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -9,7 +9,7 @@ use crate::{ #[cfg(target_os = "android")] use jnix::{FromJava, IntoJava}; use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, fmt}; +use std::{collections::HashSet, fmt, str::FromStr}; use talpid_types::net::{openvpn::ProxySettings, IpVersion, TransportProtocol, TunnelType}; pub trait Match<T> { @@ -144,6 +144,54 @@ impl<T> From<Option<T>> for Constraint<T> { } } +impl<T: fmt::Debug + Clone + FromStr> FromStr for Constraint<T> { + type Err = T::Err; + + fn from_str(value: &str) -> Result<Self, Self::Err> { + if value.eq_ignore_ascii_case("any") { + return Ok(Self::Any); + } + Ok(Self::Only(T::from_str(value)?)) + } +} + +#[cfg(feature = "clap")] +impl<T: fmt::Debug + Clone + clap::builder::ValueParserFactory> clap::builder::ValueParserFactory + for Constraint<T> +where + <T as clap::builder::ValueParserFactory>::Parser: Sync + Send + Clone, +{ + type Parser = ConstraintParser<T::Parser>; + + fn value_parser() -> Self::Parser { + ConstraintParser(T::value_parser()) + } +} + +#[cfg(feature = "clap")] +#[derive(fmt::Debug, Clone)] +pub struct ConstraintParser<T>(T); + +#[cfg(feature = "clap")] +impl<T: clap::builder::TypedValueParser> clap::builder::TypedValueParser for ConstraintParser<T> +where + T::Value: fmt::Debug, +{ + type Value = Constraint<T::Value>; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result<Self::Value, clap::Error> { + if value.eq_ignore_ascii_case("any") { + return Ok(Constraint::Any); + } + self.0.parse_ref(cmd, arg, value).map(Constraint::Only) + } +} + /// Specifies a specific endpoint or [`RelayConstraints`] to use when `mullvad-daemon` selects a /// relay. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] @@ -352,7 +400,8 @@ impl Set<LocationConstraint> for LocationConstraint { } /// Limits the set of servers to choose based on ownership. -#[derive(Copy, Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum Ownership { MullvadOwned, Rented, @@ -376,6 +425,24 @@ impl fmt::Display for Ownership { } } +impl FromStr for Ownership { + type Err = OwnershipParseError; + + fn from_str(s: &str) -> Result<Ownership, Self::Err> { + match s { + "owned" | "mullvad-owned" => Ok(Ownership::MullvadOwned), + "rented" => Ok(Ownership::Rented), + _ => Err(OwnershipParseError), + } + } +} + +/// Returned when `Ownership::from_str` fails to convert a string into a +/// [`Ownership`] object. +#[derive(err_derive::Error, Debug, Clone, PartialEq, Eq)] +#[error(display = "Not a valid ownership setting")] +pub struct OwnershipParseError; + /// Limits the set of [`crate::relay_list::Relay`]s used by a `RelaySelector` based on /// provider. pub type Provider = String; @@ -514,6 +581,7 @@ pub enum BridgeSettings { #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum SelectedObfuscation { Auto, #[default] @@ -588,6 +656,7 @@ impl fmt::Display for BridgeConstraints { /// Setting indicating whether to connect to a bridge server, or to handle it automatically. #[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum BridgeState { Auto, On, diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index 5d9c082e2e..80891ea970 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -65,7 +65,7 @@ impl Serialize for SettingsVersion { #[cfg_attr(target_os = "android", derive(IntoJava))] #[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] pub struct Settings { - relay_settings: RelaySettings, + pub relay_settings: RelaySettings, #[cfg_attr(target_os = "android", jnix(skip))] pub bridge_settings: BridgeSettings, #[cfg_attr(target_os = "android", jnix(skip))] @@ -98,7 +98,7 @@ pub struct Settings { pub wg_migration_rand_num: f32, /// Specifies settings schema version #[cfg_attr(target_os = "android", jnix(skip))] - settings_version: SettingsVersion, + pub settings_version: SettingsVersion, } fn out_of_range_wg_migration_rand_num() -> f32 { @@ -165,10 +165,6 @@ impl Settings { self.relay_settings = new_settings; } } - - pub fn get_settings_version(&self) -> SettingsVersion { - self.settings_version - } } /// TunnelOptions holds configuration data that applies to all kinds of tunnels. diff --git a/mullvad-types/src/wireguard.rs b/mullvad-types/src/wireguard.rs index 7f12ed42f1..2e699c344e 100644 --- a/mullvad-types/src/wireguard.rs +++ b/mullvad-types/src/wireguard.rs @@ -3,7 +3,7 @@ use chrono::{offset::Utc, DateTime}; #[cfg(target_os = "android")] use jnix::IntoJava; use serde::{Deserialize, Deserializer, Serialize}; -use std::{convert::TryFrom, fmt, time::Duration}; +use std::{convert::TryFrom, fmt, str::FromStr, time::Duration}; use talpid_types::net::wireguard; pub const MIN_ROTATION_INTERVAL: Duration = Duration::from_secs(1 * 24 * 60 * 60); @@ -16,6 +16,7 @@ const QUANTUM_RESISTANT_AUTO_STATE: bool = false; #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] pub enum QuantumResistantState { Auto, On, @@ -32,6 +33,25 @@ impl fmt::Display for QuantumResistantState { } } +impl FromStr for QuantumResistantState { + type Err = QuantumResistantStateParseError; + + fn from_str(s: &str) -> Result<QuantumResistantState, Self::Err> { + match s { + "any" | "auto" => Ok(QuantumResistantState::Auto), + "on" => Ok(QuantumResistantState::On), + "off" => Ok(QuantumResistantState::Off), + _ => Err(QuantumResistantStateParseError), + } + } +} + +/// Returned when `QuantumResistantState::from_str` fails to convert a string into a +/// [`QuantumResistantState`] object. +#[derive(err_derive::Error, Debug, Clone, PartialEq, Eq)] +#[error(display = "Not a valid state")] +pub struct QuantumResistantStateParseError; + /// Contains account specific wireguard data #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct WireguardData { @@ -120,6 +140,36 @@ impl TryFrom<Duration> for RotationInterval { } } +impl fmt::Display for RotationInterval { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} hours", self.as_duration().as_secs() / 60 / 60) + } +} + +#[cfg(feature = "clap")] +impl clap::builder::ValueParserFactory for RotationInterval { + type Parser = clap::builder::RangedU64ValueParser<RotationInterval>; + + fn value_parser() -> Self::Parser { + clap::builder::RangedU64ValueParser::new().range( + (MIN_ROTATION_INTERVAL.as_secs() / 60 / 60) + ..(MAX_ROTATION_INTERVAL.as_secs() / 60 / 60), + ) + } +} + +impl TryFrom<u64> for RotationInterval { + type Error = RotationIntervalError; + + fn try_from(value: u64) -> Result<Self, Self::Error> { + // Convert a u64, specified in hours, to a `RotationInterval` + let val = value + .checked_mul(60 * 60) + .ok_or(RotationIntervalError::TooLarge)?; + RotationInterval::new(Duration::from_secs(val)) + } +} + impl From<RotationInterval> for Duration { fn from(interval: RotationInterval) -> Duration { *interval.as_duration() diff --git a/talpid-core/src/split_tunnel/windows/mod.rs b/talpid-core/src/split_tunnel/windows/mod.rs index 022d66fed1..40d7c340ad 100644 --- a/talpid-core/src/split_tunnel/windows/mod.rs +++ b/talpid-core/src/split_tunnel/windows/mod.rs @@ -20,7 +20,7 @@ use std::{ time::Duration, }; use talpid_routing::{get_best_default_route, CallbackHandle, EventType, RouteManagerHandle}; -use talpid_types::{tunnel::ErrorStateCause, ErrorExt}; +use talpid_types::{split_tunnel::ExcludedProcess, tunnel::ErrorStateCause, ErrorExt}; use talpid_windows_net::{get_ip_address_for_interface, AddressFamily}; use windows_sys::Win32::Foundation::ERROR_OPERATION_ABORTED; @@ -131,18 +131,6 @@ struct InterfaceAddresses { internet_ipv6: Option<Ipv6Addr>, } -/// Represents a process that is being excluded from the tunnel. -#[derive(Debug, Clone)] -pub struct ExcludedProcess { - /// Process identifier. - pub pid: u32, - /// Path to the image that this process is an instance of. - pub image: PathBuf, - /// If true, then the process is split because its parent was split, - /// not due to its path being in the config. - pub inherited: bool, -} - /// Cloneable handle for interacting with the split tunnel module. #[derive(Debug, Clone)] pub struct SplitTunnelHandle { diff --git a/talpid-types/src/lib.rs b/talpid-types/src/lib.rs index 1aab0849e5..92c0a2ce76 100644 --- a/talpid-types/src/lib.rs +++ b/talpid-types/src/lib.rs @@ -10,6 +10,9 @@ pub mod tunnel; #[cfg(target_os = "linux")] pub mod cgroup; +#[cfg(target_os = "windows")] +pub mod split_tunnel; + /// Used to generate string representations of error chains. pub trait ErrorExt { /// Creates a string representation of the entire error chain. diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs index 126b721105..910c1f168e 100644 --- a/talpid-types/src/net/mod.rs +++ b/talpid-types/src/net/mod.rs @@ -125,6 +125,24 @@ impl fmt::Display for TunnelType { } } +impl FromStr for TunnelType { + type Err = TunnelTypeParseError; + + fn from_str(s: &str) -> Result<TunnelType, Self::Err> { + match s { + "openvpn" => Ok(TunnelType::OpenVpn), + "wireguard" => Ok(TunnelType::Wireguard), + _ => Err(TunnelTypeParseError), + } + } +} + +/// Returned when `TunnelType::from_str` fails to convert a string into a +/// [`TunnelType`] object. +#[derive(err_derive::Error, Debug, Clone, PartialEq, Eq)] +#[error(display = "Not a valid tunnel protocol")] +pub struct TunnelTypeParseError; + /// A tunnel endpoint is broadcast during the connecting and connected states of the tunnel state /// machine. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -321,6 +339,24 @@ impl fmt::Display for IpVersion { } } +impl FromStr for IpVersion { + type Err = IpVersionParseError; + + fn from_str(s: &str) -> Result<IpVersion, Self::Err> { + match s { + "v4" | "ipv4" => Ok(IpVersion::V4), + "v6" | "ipv6" => Ok(IpVersion::V6), + _ => Err(IpVersionParseError), + } + } +} + +/// Returned when `IpVersion::from_str` fails to convert a string into a +/// [`IpVersion`] object. +#[derive(err_derive::Error, Debug, Clone, PartialEq, Eq)] +#[error(display = "Not a valid IP protocol")] +pub struct IpVersionParseError; + /// Representation of a transport protocol, either UDP or TCP. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -337,11 +373,13 @@ impl FromStr for TransportProtocol { type Err = TransportProtocolParseError; fn from_str(s: &str) -> std::result::Result<TransportProtocol, Self::Err> { - match s { - "udp" => Ok(TransportProtocol::Udp), - "tcp" => Ok(TransportProtocol::Tcp), - _ => Err(TransportProtocolParseError), + if s.eq_ignore_ascii_case("udp") { + return Ok(TransportProtocol::Udp); } + if s.eq_ignore_ascii_case("tcp") { + return Ok(TransportProtocol::Tcp); + } + Err(TransportProtocolParseError) } } @@ -356,15 +394,10 @@ impl fmt::Display for TransportProtocol { /// Returned when `TransportProtocol::from_str` fails to convert a string into a /// [`TransportProtocol`] object. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(err_derive::Error, Debug, Clone, PartialEq, Eq)] +#[error(display = "Not a valid transport protocol")] pub struct TransportProtocolParseError; -impl fmt::Display for TransportProtocolParseError { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.write_str("Not a valid transport protocol") - } -} - /// Holds optional settings that can apply to different kinds of tunnels #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct GenericTunnelOptions { diff --git a/talpid-types/src/net/openvpn.rs b/talpid-types/src/net/openvpn.rs index 54a4feb044..5968331b52 100644 --- a/talpid-types/src/net/openvpn.rs +++ b/talpid-types/src/net/openvpn.rs @@ -134,7 +134,7 @@ impl ShadowsocksProxySettings { /// List of ciphers usable by a Shadowsocks proxy. /// Cf. [`ShadowsocksProxySettings::cipher`]. -pub static SHADOWSOCKS_CIPHERS: &[&str] = &[ +pub const SHADOWSOCKS_CIPHERS: [&str; 19] = [ // Stream ciphers. "aes-128-cfb", "aes-128-cfb1", diff --git a/talpid-types/src/net/wireguard.rs b/talpid-types/src/net/wireguard.rs index 181e647cf7..efa35e9eef 100644 --- a/talpid-types/src/net/wireguard.rs +++ b/talpid-types/src/net/wireguard.rs @@ -104,6 +104,10 @@ impl PrivateKey { pub fn to_base64(&self) -> String { base64::encode(self.0.to_bytes()) } + + pub fn from_base64(key: &str) -> Result<Self, InvalidKey> { + key_from_base64(key) + } } impl From<[u8; 32]> for PrivateKey { @@ -154,9 +158,14 @@ impl<'de> Deserialize<'de> for PrivateKey { #[derive(Clone)] pub struct PublicKey(x25519_dalek::PublicKey); -/// Error returned if a base64 string represents an invalid key -#[derive(Debug)] -pub struct InvalidKeyError(()); +/// Error returned if an input represents an invalid key +#[derive(Debug, err_derive::Error)] +pub enum InvalidKey { + #[error(display = "Invalid key: {}", _0)] + Format(#[error(source)] base64::DecodeError), + #[error(display = "Invalid key length: {}", _0)] + Length(usize), +} impl PublicKey { /// Get the public key as bytes @@ -168,14 +177,8 @@ impl PublicKey { base64::encode(self.as_bytes()) } - pub fn from_base64(key: &str) -> Result<Self, InvalidKeyError> { - let bytes = base64::decode(key).map_err(|_| InvalidKeyError(()))?; - if bytes.len() != 32 { - return Err(InvalidKeyError(())); - } - let mut key = [0u8; 32]; - key.copy_from_slice(&bytes); - Ok(From::from(key)) + pub fn from_base64(key: &str) -> Result<Self, InvalidKey> { + key_from_base64(key) } } @@ -191,6 +194,16 @@ impl From<[u8; 32]> for PublicKey { } } +impl TryFrom<&[u8]> for PublicKey { + type Error = InvalidKey; + + fn try_from(public_key: &[u8]) -> Result<PublicKey, Self::Error> { + let key: [u8; 32] = + <[u8; 32]>::try_from(public_key).map_err(|_| InvalidKey::Length(public_key.len()))?; + Ok(PublicKey(x25519_dalek::PublicKey::from(key))) + } +} + impl Serialize for PublicKey { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where @@ -275,16 +288,15 @@ where use serde::de::Error; String::deserialize(deserializer) - .and_then(|string| base64::decode(string).map_err(|err| Error::custom(err.to_string()))) - .and_then(|buffer| { - let mut key = [0u8; 32]; - if buffer.len() != 32 { - return Err(Error::custom(format!( - "Key has unexpected length: {}", - buffer.len() - ))); - } - key.copy_from_slice(&buffer); - Ok(From::from(key)) - }) + .and_then(|string| key_from_base64(&string).map_err(|err| Error::custom(err.to_string()))) +} + +fn key_from_base64<K: From<[u8; 32]>>(key: &str) -> Result<K, InvalidKey> { + let bytes = base64::decode(key).map_err(InvalidKey::Format)?; + if bytes.len() != 32 { + return Err(InvalidKey::Length(bytes.len())); + } + let mut key = [0u8; 32]; + key.copy_from_slice(&bytes); + Ok(From::from(key)) } diff --git a/talpid-types/src/split_tunnel.rs b/talpid-types/src/split_tunnel.rs new file mode 100644 index 0000000000..4b4d6576f3 --- /dev/null +++ b/talpid-types/src/split_tunnel.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +/// A process that is being excluded from the tunnel. +#[derive(Debug, Clone)] +pub struct ExcludedProcess { + /// Process identifier. + pub pid: u32, + /// Path to the image that this process is an instance of. + pub image: PathBuf, + /// If true, then the process is split because its parent was split, + /// not due to its path being in the config. + pub inherited: bool, +} |
