summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2023-05-03 11:20:31 +0200
committerDavid Lönnhager <david.l@mullvad.net>2023-05-03 11:20:31 +0200
commit49ea114adddba1a1db6ffc6c440e743c01797a47 (patch)
tree66f1bf1e3e1d208e233e5622045503abe85a3a89
parentbeaa6d3b80d9c9dfed99c710c793830db3ddc7ec (diff)
parentaade46c9c73c874e4153caa450e713d8f8b37760 (diff)
downloadmullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.tar.xz
mullvadvpn-49ea114adddba1a1db6ffc6c440e743c01797a47.zip
Merge branch 'update-clap'
-rw-r--r--CHANGELOG.md9
-rw-r--r--Cargo.lock326
-rw-r--r--mullvad-cli/Cargo.toml8
-rw-r--r--mullvad-cli/src/cmds/account.rs434
-rw-r--r--mullvad-cli/src/cmds/auto_connect.rs63
-rw-r--r--mullvad-cli/src/cmds/beta_program.rs84
-rw-r--r--mullvad-cli/src/cmds/block_when_disconnected.rs68
-rw-r--r--mullvad-cli/src/cmds/bridge.rs623
-rw-r--r--mullvad-cli/src/cmds/connect.rs50
-rw-r--r--mullvad-cli/src/cmds/disconnect.rs47
-rw-r--r--mullvad-cli/src/cmds/dns.rs256
-rw-r--r--mullvad-cli/src/cmds/lan.rs69
-rw-r--r--mullvad-cli/src/cmds/lockdown.rs36
-rw-r--r--mullvad-cli/src/cmds/mod.rs138
-rw-r--r--mullvad-cli/src/cmds/obfuscation.rs177
-rw-r--r--mullvad-cli/src/cmds/reconnect.rs50
-rw-r--r--mullvad-cli/src/cmds/relay.rs1209
-rw-r--r--mullvad-cli/src/cmds/relay_constraints.rs34
-rw-r--r--mullvad-cli/src/cmds/reset.rs60
-rw-r--r--mullvad-cli/src/cmds/split_tunnel/linux.rs99
-rw-r--r--mullvad-cli/src/cmds/split_tunnel/windows.rs207
-rw-r--r--mullvad-cli/src/cmds/status.rs184
-rw-r--r--mullvad-cli/src/cmds/tunnel.rs534
-rw-r--r--mullvad-cli/src/cmds/tunnel_state.rs85
-rw-r--r--mullvad-cli/src/cmds/version.rs77
-rw-r--r--mullvad-cli/src/location.rs81
-rw-r--r--mullvad-cli/src/main.rs244
-rw-r--r--mullvad-cli/src/state.rs43
-rw-r--r--mullvad-daemon/Cargo.toml2
-rw-r--r--mullvad-daemon/src/cli.rs164
-rw-r--r--mullvad-daemon/src/lib.rs6
-rw-r--r--mullvad-daemon/src/management_interface.rs25
-rw-r--r--mullvad-daemon/src/rpc_uniqueness_check.rs4
-rw-r--r--mullvad-jni/src/problem_report.rs2
-rw-r--r--mullvad-management-interface/src/client.rs552
-rw-r--r--mullvad-management-interface/src/lib.rs40
-rw-r--r--mullvad-management-interface/src/types/conversions/account.rs59
-rw-r--r--mullvad-management-interface/src/types/conversions/device.rs81
-rw-r--r--mullvad-management-interface/src/types/conversions/mod.rs6
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_list.rs174
-rw-r--r--mullvad-management-interface/src/types/conversions/settings.rs101
-rw-r--r--mullvad-management-interface/src/types/conversions/split_tunnel.rs23
-rw-r--r--mullvad-management-interface/src/types/conversions/version.rs17
-rw-r--r--mullvad-management-interface/src/types/conversions/wireguard.rs19
-rw-r--r--mullvad-nsis/Cargo.toml2
-rw-r--r--mullvad-problem-report/Cargo.toml2
-rw-r--r--mullvad-problem-report/src/lib.rs4
-rw-r--r--mullvad-problem-report/src/main.rs172
-rw-r--r--mullvad-setup/Cargo.toml2
-rw-r--r--mullvad-setup/src/main.rs75
-rw-r--r--mullvad-types/Cargo.toml2
-rw-r--r--mullvad-types/src/device.rs11
-rw-r--r--mullvad-types/src/relay_constraints.rs73
-rw-r--r--mullvad-types/src/settings/mod.rs8
-rw-r--r--mullvad-types/src/wireguard.rs52
-rw-r--r--talpid-core/src/split_tunnel/windows/mod.rs14
-rw-r--r--talpid-types/src/lib.rs3
-rw-r--r--talpid-types/src/net/mod.rs55
-rw-r--r--talpid-types/src/net/openvpn.rs2
-rw-r--r--talpid-types/src/net/wireguard.rs58
-rw-r--r--talpid-types/src/split_tunnel.rs13
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,
+}