summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock403
-rw-r--r--Cargo.toml2
-rw-r--r--desktop/mullvad-daemon-portable/.gitignore2
-rwxr-xr-xdesktop/mullvad-daemon-portable/build-image.sh39
-rwxr-xr-xdesktop/mullvad-daemon-portable/mullvad-daemon-staticbin0 -> 16604776 bytes
-rw-r--r--desktop/mullvad-daemon-portable/mullvad-daemon.service20
-rw-r--r--desktop/mullvad-daemon-portable/portablectl-rs/Cargo.toml19
-rw-r--r--desktop/mullvad-daemon-portable/portablectl-rs/src/bus.rs77
-rw-r--r--desktop/mullvad-daemon-portable/portablectl-rs/src/main.rs127
-rw-r--r--desktop/mullvad-daemon-portable/repart.d/10-root.conf19
-rw-r--r--desktop/mullvad-daemon-portable/test.sh7
11 files changed, 696 insertions, 19 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 64a7c98627..c82ac9c81e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -165,6 +165,18 @@ dependencies = [
]
[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
name = "async-channel"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -177,6 +189,96 @@ dependencies = [
]
[[package]]
+name = "async-executor"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 1.1.3",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.3",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.3",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1198,6 +1300,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
name = "enum-as-inner"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1230,6 +1338,27 @@ dependencies = [
]
[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1275,12 +1404,12 @@ dependencies = [
[[package]]
name = "errno"
-version = "0.3.8"
+version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1457,7 +1586,10 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
dependencies = [
+ "fastrand",
"futures-core",
+ "futures-io",
+ "parking",
"pin-project-lite",
]
@@ -1689,9 +1821,9 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.14.3"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
@@ -1712,6 +1844,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2156,9 +2294,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
-version = "2.2.6"
+version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown",
@@ -2478,9 +2616,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.172"
+version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libdbus-sys"
@@ -2540,6 +2678,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2702,7 +2846,7 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
- "hermit-abi",
+ "hermit-abi 0.3.9",
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
@@ -3695,6 +3839,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
name = "os_info"
version = "3.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4020,6 +4174,20 @@ dependencies = [
]
[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi 0.5.2",
+ "pin-project-lite",
+ "rustix 1.1.3",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4049,6 +4217,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
+name = "portablectl-rs"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "serde",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "zbus",
+]
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4112,6 +4293,15 @@ dependencies = [
]
[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+dependencies = [
+ "toml_edit 0.23.10+spec-1.0.0",
+]
+
+[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4558,13 +4748,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.9.0",
- "errno 0.3.8",
+ "errno 0.3.14",
"libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.13",
"windows-sys 0.52.0",
]
[[package]]
+name = "rustix"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
+dependencies = [
+ "bitflags 2.9.0",
+ "errno 0.3.14",
+ "libc",
+ "linux-raw-sys 0.11.0",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "rustls"
version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4727,6 +4930,17 @@ dependencies = [
]
[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5386,7 +5600,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
- "rustix",
+ "rustix 0.38.34",
"windows-sys 0.52.0",
]
@@ -5557,6 +5771,7 @@ dependencies = [
"signal-hook-registry",
"socket2 0.5.8",
"tokio-macros",
+ "tracing",
"windows-sys 0.52.0",
]
@@ -5668,8 +5883,8 @@ checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
- "toml_datetime",
- "toml_edit",
+ "toml_datetime 0.6.8",
+ "toml_edit 0.22.20",
]
[[package]]
@@ -5682,6 +5897,15 @@ dependencies = [
]
[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
name = "toml_edit"
version = "0.22.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5690,8 +5914,29 @@ dependencies = [
"indexmap",
"serde",
"serde_spanned",
- "toml_datetime",
- "winnow",
+ "toml_datetime 0.6.8",
+ "winnow 0.6.20",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.23.10+spec-1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
+dependencies = [
+ "indexmap",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
+dependencies = [
+ "winnow 0.7.14",
]
[[package]]
@@ -6012,6 +6257,17 @@ dependencies = [
]
[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset 0.9.1",
+ "tempfile",
+ "winapi",
+]
+
+[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6260,7 +6516,7 @@ dependencies = [
"either",
"home",
"once_cell",
- "rustix",
+ "rustix 0.38.34",
]
[[package]]
@@ -6962,6 +7218,15 @@ dependencies = [
]
[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7092,6 +7357,68 @@ dependencies = [
]
[[package]]
+name = "zbus"
+version = "5.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix 1.1.3",
+ "serde",
+ "serde_repr",
+ "tokio",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 0.7.14",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
+dependencies = [
+ "serde",
+ "winnow 0.7.14",
+ "zvariant",
+]
+
+[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7173,3 +7500,43 @@ dependencies = [
"quote",
"syn 2.0.108",
]
+
+[[package]]
+name = "zvariant"
+version = "5.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 0.7.14",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.108",
+ "winnow 0.7.14",
+]
diff --git a/Cargo.toml b/Cargo.toml
index e011166d98..e1563f51b4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = [
- "android/translations-converter",
+ "android/translations-converter", "desktop/mullvad-daemon-portable/portablectl-rs",
"desktop/packages/nseventforwarder",
"desktop/packages/windows-utils",
"installer-downloader",
diff --git a/desktop/mullvad-daemon-portable/.gitignore b/desktop/mullvad-daemon-portable/.gitignore
new file mode 100644
index 0000000000..dc932dc2ea
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/.gitignore
@@ -0,0 +1,2 @@
+rootfs
+*.raw
diff --git a/desktop/mullvad-daemon-portable/build-image.sh b/desktop/mullvad-daemon-portable/build-image.sh
new file mode 100755
index 0000000000..ade04b6211
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/build-image.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+set -ex
+
+OUTFILE="mullvad-portable.raw"
+DESTDIR="./rootfs"
+
+cd "$(dirname "$0")"
+
+rm -rf "$DESTDIR" "$OUTFILE"
+mkdir "$DESTDIR"
+
+mkdir -p \
+ "$DESTDIR/etc" \
+ "$DESTDIR/usr/bin" \
+ "$DESTDIR/usr/lib/systemd/system" \
+ "$DESTDIR/var/log" \
+ "$DESTDIR/var/tmp" \
+ "$DESTDIR/proc" \
+ "$DESTDIR/sys" \
+ "$DESTDIR/dev" \
+ "$DESTDIR/run" \
+ "$DESTDIR/tmp"
+
+# TODO: compile daemon statically
+cp mullvad-daemon-static "$DESTDIR/usr/bin/mullvad-daemon"
+
+cp mullvad-daemon.service "$DESTDIR/usr/lib/systemd/system/mullvad-portable.service"
+
+cp /usr/lib/os-release "$DESTDIR/usr/lib/os-release"
+cat >> "$DESTDIR/usr/lib/os-release" <<EOF
+PORTABLE_PRETTY_NAME="Mullvad Daemon (portable)"
+EOF
+
+touch "$DESTDIR/etc/resolv.conf"
+touch "$DESTDIR/etc/machine-id"
+
+systemd-repart --definitions repart.d --empty=create --size=auto --copy-source "$DESTDIR" mullvad-portable.raw
+
diff --git a/desktop/mullvad-daemon-portable/mullvad-daemon-static b/desktop/mullvad-daemon-portable/mullvad-daemon-static
new file mode 100755
index 0000000000..c905045c7b
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/mullvad-daemon-static
Binary files differ
diff --git a/desktop/mullvad-daemon-portable/mullvad-daemon.service b/desktop/mullvad-daemon-portable/mullvad-daemon.service
new file mode 100644
index 0000000000..e36bd90eff
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/mullvad-daemon.service
@@ -0,0 +1,20 @@
+# Systemd service unit file for the Mullvad VPN daemon
+# testing if new changes are added
+
+[Unit]
+Description=Mullvad VPN daemon
+Before=network-online.target
+After=mullvad-early-boot-blocking.service NetworkManager.service systemd-resolved.service
+
+StartLimitBurst=5
+StartLimitIntervalSec=20
+RequiresMountsFor=/opt/Mullvad\x20VPN/resources/
+
+[Service]
+Restart=always
+RestartSec=1
+ExecStart=/usr/bin/mullvad-daemon -v --disable-stdout-timestamps
+Environment="MULLVAD_RESOURCE_DIR=/opt/Mullvad VPN/resources/"
+
+[Install]
+WantedBy=multi-user.target
diff --git a/desktop/mullvad-daemon-portable/portablectl-rs/Cargo.toml b/desktop/mullvad-daemon-portable/portablectl-rs/Cargo.toml
new file mode 100644
index 0000000000..f4b4e56499
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/portablectl-rs/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "portablectl-rs"
+version = "0.1.0"
+edition.workspace = true
+rust-version.workspace = true
+repository.workspace = true
+license.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+clap = { workspace = true, features = ["derive", "env"] }
+serde = { workspace = true, features = ["derive"] }
+tokio = { workspace = true, features = ["full"] }
+tracing = "0.1.44"
+tracing-subscriber = { workspace = true, features = ["env-filter"] }
+zbus = { version = "5.13.2", features = ["tokio"] }
+
+[lints]
+workspace = true
diff --git a/desktop/mullvad-daemon-portable/portablectl-rs/src/bus.rs b/desktop/mullvad-daemon-portable/portablectl-rs/src/bus.rs
new file mode 100644
index 0000000000..5119b68896
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/portablectl-rs/src/bus.rs
@@ -0,0 +1,77 @@
+use serde::Deserialize;
+use zbus::zvariant::OwnedObjectPath;
+
+#[zbus::proxy(
+ interface = "org.freedesktop.portable1.Manager",
+ default_service = "org.freedesktop.portable1",
+ default_path = "/org/freedesktop/portable1"
+)]
+
+pub trait PortabledD {
+ // TODO: return type is object-path
+ async fn get_image(&self, name: &str) -> zbus::Result<OwnedObjectPath>;
+ async fn list_images(&self) -> zbus::Result<Vec<ImageListing>>;
+
+ /// Attach a portable service image.
+ ///
+ /// - `image` refers to a path or a name.
+ /// - if `runtime=true`, the image will not persist across reboots.
+ /// - `copy_mode` is one of `""`, `"copy"`, `"symlink"` `"mixed"`.
+ ///
+ /// ## dbus type signature:
+ /// ```systemd
+ /// AttachImage(in s image,
+ /// in as matches,
+ /// in s profile,
+ /// in b runtime,
+ /// in s copy_mode,
+ /// out a(sss) changes);
+ /// ```
+ #[zbus(allow_interactive_auth)]
+ async fn attach_image(
+ &self,
+ image: &str,
+ matches: &[&str],
+ profile: &str,
+ runtime: bool,
+ copy_mode: &str,
+ ) -> zbus::Result<Vec<Change>>;
+
+ /// Detach a portable service image.
+ ///
+ /// - `image` refers to a path or a name.
+ /// - `runtime` indicates whether the image was attached only for the current boot session.
+ ///
+ /// ## dbus type signature:
+ /// ```systemd
+ /// AttachImage(in s image,
+ /// in as matches,
+ /// in s profile,
+ /// in b runtime,
+ /// in s copy_mode,
+ /// out a(sss) changes);
+ /// ```
+ #[zbus(allow_interactive_auth)]
+ async fn detach_image(&self, image: &str, runtime: bool) -> zbus::Result<Vec<Change>>;
+}
+
+/// A change that was applied to the system as a result of `PortableD::attach_image`.
+#[derive(Clone, Debug, Deserialize, zbus::zvariant::Type)]
+pub struct Change {
+ pub change_type: String,
+ pub path: String,
+ pub source: String,
+}
+
+#[derive(Clone, Debug, Deserialize, zbus::zvariant::Type)]
+pub struct ImageListing {
+ // TODO: field names
+ pub name: String,
+ pub image_type: String,
+ pub read_only: bool,
+ pub creation_time: u64,
+ pub modification_time: u64,
+ pub current_disk_space: u64,
+ pub usage: String,
+ pub object_path: OwnedObjectPath, // TODO: type is object-path
+}
diff --git a/desktop/mullvad-daemon-portable/portablectl-rs/src/main.rs b/desktop/mullvad-daemon-portable/portablectl-rs/src/main.rs
new file mode 100644
index 0000000000..1b4e90a3a1
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/portablectl-rs/src/main.rs
@@ -0,0 +1,127 @@
+use std::path::Path;
+
+use anyhow::Context;
+use clap::Parser;
+
+use crate::bus::Change;
+
+mod bus;
+
+#[derive(Parser)]
+struct Opt {
+ #[clap(long, env = "RUST_LOG", default_value = "info")]
+ log_filter: String,
+
+ #[clap(subcommand)]
+ command: Command,
+}
+
+#[derive(clap::Subcommand)]
+enum Command {
+ List,
+ Attach {
+ image: String,
+ #[clap(long, default_value = "default")]
+ profile: String,
+ },
+ Detach {
+ image: String,
+ },
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let opt = Opt::parse();
+
+ let fmt_subscriber = tracing_subscriber::FmtSubscriber::builder()
+ .with_env_filter(&opt.log_filter)
+ .finish();
+ tracing::subscriber::set_global_default(fmt_subscriber)
+ .context("Failed to initialize tracing subscriber")?;
+
+ tracing::info!("Connecting to dbus");
+ let connection = zbus::connection::Builder::system()?
+ .auth_mechanism(zbus::AuthMechanism::External)
+ .build()
+ .await
+ .context("Failed to connect to session dbus")?;
+
+ tracing::info!("Connecting to portabled");
+ let portabled = bus::PortabledDProxy::new(&connection)
+ .await
+ .context("Failed to connect to dbus service")?;
+
+ match opt.command {
+ Command::List => {
+ tracing::info!("Listing images");
+ for image in portabled
+ .list_images()
+ .await
+ .context("Failed to list portabled images")?
+ {
+ tracing::info!("{image:#?}");
+ }
+ }
+ Command::Attach { image, profile } => {
+ let image = if image.contains('/') {
+ let path = Path::new(&image);
+ let path = path.canonicalize().context("Failed to canonicalize path")?;
+ path.to_str()
+ .context("Path was not valid utf-8")?
+ .to_string()
+ } else {
+ image
+ };
+
+ let unit_file_matches = &[];
+ let runtime = true;
+ let changes = portabled
+ .attach_image(
+ &image,
+ unit_file_matches,
+ &profile,
+ runtime,
+ "", // TODO
+ )
+ .await
+ .context("Failed to attach portable service image")?;
+
+ log_changes(&changes);
+ }
+ Command::Detach { image } => {
+ let image = if image.contains('/') {
+ let path = Path::new(&image);
+ let path = path.canonicalize().context("Failed to canonicalize path")?;
+ path.to_str()
+ .context("Path was not valid utf-8")?
+ .to_string()
+ } else {
+ image
+ };
+
+ let runtime = true;
+ let changes = portabled
+ .detach_image(&image, runtime)
+ .await
+ .context("Failed to detach portable service")?;
+ log_changes(&changes);
+ }
+ }
+
+ Ok(())
+}
+
+fn log_changes(changes: &[Change]) {
+ for change in changes {
+ if change.source.is_empty() {
+ tracing::info!("- {} {:?}", change.change_type, change.path);
+ } else {
+ tracing::info!(
+ "- {} {:?} -> {:?}",
+ change.change_type,
+ change.source,
+ change.path
+ );
+ }
+ }
+}
diff --git a/desktop/mullvad-daemon-portable/repart.d/10-root.conf b/desktop/mullvad-daemon-portable/repart.d/10-root.conf
new file mode 100644
index 0000000000..e94f0f8fd0
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/repart.d/10-root.conf
@@ -0,0 +1,19 @@
+[Partition]
+Type=root
+Format=btrfs
+Label=root
+
+SizeMinBytes=1M
+SizeMaxBytes=1G
+
+# don't sign the image
+Verity=off
+
+CopyFiles=/dev
+CopyFiles=/etc
+CopyFiles=/proc
+CopyFiles=/run
+CopyFiles=/sys
+CopyFiles=/tmp
+CopyFiles=/usr
+CopyFiles=/var
diff --git a/desktop/mullvad-daemon-portable/test.sh b/desktop/mullvad-daemon-portable/test.sh
new file mode 100644
index 0000000000..6ea8d65c64
--- /dev/null
+++ b/desktop/mullvad-daemon-portable/test.sh
@@ -0,0 +1,7 @@
+systemctl stop mullvad-portable.service
+portablectl detach ./mullvad-portable.service
+portablectl attach --profile=trusted ./mullvad-portable.service
+systemctl start mullvad-portable.service
+systemctl status mullvad-portable.service
+journalctl -xefu mullvad-portable.service
+