File: README.md
Mullvad VPN desktop and mobile app
Welcome to the Mullvad VPN client app source code repository.
This is the VPN client software for the Mullvad VPN service.
For more information about the service, please visit our website,
mullvad.net (Also accessible via Tor on our
onion service).
This repository contains all the source code for the
desktop and mobile versions of the app. For desktop this includes the system service/daemon
(mullvad-daemon), a graphical user interface (GUI) and a command
line interface (CLI). The Android app uses the same backing system service for the
tunnel and security but has a dedicated frontend in android/. iOS consists of a
completely standalone implementation that resides in ios/.
Releases
There are built and signed releases for macOS, Windows, Linux and Android available on
our website and on
GitHub. The Android app is also available
on Google Play and F-Droid and the iOS version on App Store.
You can find our code signing keys as well as instructions for how to cryptographically verify
your download on Mullvad's Open Source page.
Platform/OS support
See Supported Platforms for details on which operating
systems, versions and architectures are supported, and which ones are covered by our
automated test suite.
Features
Here is a table containing the features of the app across platforms. This is intended to reflect
the current state of the latest code in git, not necessarily any existing release.
| Windows | Linux | macOS | Android | iOS | |
|---|---|---|---|---|---|
| WireGuard | โ | โ | โ | โ | โ |
| Quantum-resistant tunnels | โ | โ | โ | โ | โ |
| DAITA | โ | โ | โ | โ | โ |
| WireGuard multihop | โ | โ | โ | โ | โ |
| WireGuard over TCP | โ | โ | โ | โ | โ |
| WireGuard over Shadowsocks | โ | โ | โ | โ | โ |
| WireGuard over QUIC | โ | โ | โ | โ | โ |
| Lightweight WireGuard Obfuscation (LWO) | โ | โ | โ | โ | โ |
| Split tunneling | โ | โ | โ | โ | |
| Custom DNS server | โ | โ | โ | โ | โ |
| Content blockers (Ads etc) | โ | โ | โ | โ | โ |
| Optional local network access | โ | โ | โ | โ | โ* |
| Externally audited | โ | โ | โ | โ | โ |
* The local network is always accessible on iOS with the current implementation
User security, privacy and anonymity
This app is a privacy preserving VPN client. As such it goes to great lengths to stop traffic
leaks. And basically all settings default to the more secure/private option. The user has to
explicitly allow more loose rules if desired. See the dedicated security document for details
on what the app blocks and allows, as well as how it does it.
Secure development
Since the security of the users of the app is a top priority, by extension the security
of the development and release process also becomes a top priority. This is something we work
actively on.
Git signatures
All merge commits to the main branch must be PGP (gpg) signed in git. This signs off the entire
feature branch. The individual commits in the feature branch do not need to be signed,
unless they change one or more of the files deemed extra important.
The list of files requiring signatures to every commit that change them is defined in theverify-locked-down-signatures
workflow.
Audits, pentests and external security reviews
This app is audited by external security experts and penetration testers every second year.
We also carry out feature specific audits for certain security critical features and changes.
The results of these audits are always made public in their unredacted original form, for
full transparency towards the users. See the audits readme for this.
Moreover, we welcome any individual to review the security of this app and submit any found
issue to us. See SECURITY.md for more.
Checking out the code
This repository contains submodules needed for building the app. However, some of those submodules
also have further submodules that are quite large and not needed to build the app. So unless
you want the source code for all submodules you should avoid a recursive clone of the repository.
Instead clone the repository normally and then get one level of submodules:
git clone https://github.com/mullvad/mullvadvpn-app.git
cd mullvadvpn-app
git submodule update --initWe sign every merge commit to the main branch as well as our release tags.
If you would like to verify your checkout, you can find our developer keys on
Mullvad's Open Source page.
Binaries submodule
This repository has a git submodule at dist-assets/binaries. This submodule contains binaries and
build scripts for third party code we need to bundle with the app, such as Wintun.
This submodule conforms to the same integrity/security standards as this repository. Every merge
commit should be signed. And this main repository should only ever point to a signed merge commit
of the binaries submodule.
See the binaries submodule's README for more
details about that repository.
Building the app
See the build instructions for help building the app on desktop platforms.
For building the Android app, see the instructions for Android.
For building the iOS app, see the instructions for iOS.
Releasing the app
See this for instructions on how to make a new release.
Environment variables used by the service
TALPID_FIREWALL_DEBUG- Helps debugging the firewall. Does different things depending on
platform:- Linux: Set to
"1"to add packet counters to all firewall rules. - macOS: Makes rules log the packets they match to the
pflog0interface.- Set to
"all"to add logging to all rules. - Set to
"pass"to add logging to rules allowing packets. - Set to
"drop"to add logging to rules blocking packets.
- Set to
- Linux: Set to
TALPID_FIREWALL_DONT_SET_SRC_VALID_MARK- Set this variable to1to stop the daemon from
setting thenet.ipv4.conf.all.src_valid_markkernel parameter to1on Linux when a tunnel
is established.
The kernel config parameter is set by default, because otherwise strict reverse path filtering
may prevent relay traffic from reaching the daemon. Ifrp_filteris set to1on the interface
that will be receiving relay traffic, andsrc_valid_markis not set to1, the daemon will
not be able to receive relay traffic.TALPID_FIREWALL_DONT_SET_ARP_IGNORE- Set this variable to1to stop the daemon from
setting thenet.ipv4.conf.all.arp_ignorekernel parameter to2on Linux when a tunnel
is established.
The kernel config parameter is set by default, because otherwise an attacker who can send ARP
requests to the device running Mullvad can figure out the in-tunnel IP.TALPID_DNS_MODULE- Allows changing the method that will be used for DNS configuration.
By default this is automatically detected, but you can set it to one of the options below to
choose a specific method.Linux
"static-file": change the/etc/resolv.conffile directly"resolvconf": use theresolvconfprogram"systemd": use systemd'sresolvedservice through DBus"network-manager": useNetworkManagerservice through DBus
Windows
iphlpapi: use the IP helper APItcpip: set TCP/IP parameters in the registry
TALPID_DISABLE_LOCAL_DNS_RESOLVER- Set this variable to1to disable the local DNS resolver
(macOS only).TALPID_NEVER_FILTER_AAAA_QUERIES- Set this variable to1to never ignore DNS AAAA queries
(macOS only).TALPID_FORCE_USERSPACE_WIREGUARD- Forces the daemon to use the userspace implementation of
WireGuard.TALPID_DISABLE_OFFLINE_MONITOR- Forces the daemon to always assume the host is online.TALPID_CGROUP2_FS- On Linux, forces the daemon to look for the cgroup2 filesystem at the
specified path, instead of/sys/fs/cgroup. The cgroup2 used for split tunneling will be created
in this directory.TALPID_NET_CLS_MOUNT_DIR- On Linux, forces the daemon to mount thenet_clscontroller in the
specified directory if it isn't mounted already. This will only have an effect on older systems
where cgroup v1 is used for split tunneling.MULLVAD_MANAGEMENT_SOCKET_GROUP- On Linux and macOS, this restricts access to the management
interface UDS socket to users in the specified group. This means that only users in that group can
use the CLI and GUI. By default, everyone has access to the socket.MULLVAD_BACKTRACE_ON_FAULT- When enabled, if the daemon encounters a fault (e.g.SIGSEGV),
it will log a backtrace to stdout, and todaemon.log. By default, this is disabled in
release-builds and enabled in debug-builds. Set variable to1or0to explicitly enable or
disable this feature. Logging the backtrace causes heap allocation. Allocation is not signal safe,
but here it runs in the signal handler. This is technically undefined behavior and therefore
disabled by default. This usually works, but enable at your own risk.
Development builds only
MULLVAD_API_HOST- Set the hostname to use in API requests. E.g.api.mullvad.net.MULLVAD_API_ADDR- Set the IP address and port to use in API requests. E.g.10.10.1.2:443.MULLVAD_API_DISABLE_TLS- Use plain HTTP for API requests.MULLVAD_CONNCHECK_HOST- Set the hostname to use in connection check requests. E.g.am.i.mullvad.net.MULLVAD_ENABLE_DEV_UPDATES- Enable version checks in development builds.
Setting environment variables
Windows
Use setx from an elevated shell:
setx TALPID_DISABLE_OFFLINE 1 /mFor the change to take effect, restart the daemon:
sc.exe stop mullvadvpn
sc.exe start mullvadvpnLinux
Edit the systemd unit file via systemctl edit mullvad-daemon.service:
[Service]
Environment="TALPID_DISABLE_OFFLINE_MONITOR=1"For the change to take effect, restart the daemon:
sudo systemctl restart mullvad-daemonmacOS
Use plutil:
sudo plutil -replace EnvironmentVariables -json '{"TALPID_DISABLE_OFFLINE_MONITOR": "1"}' /Library/LaunchDaemons/net.mullvad.daemon.plistFor the change to take effect, restart the daemon:
launchctl unload -w /Library/LaunchDaemons/net.mullvad.daemon.plist
launchctl load -w /Library/LaunchDaemons/net.mullvad.daemon.plistEnvironment variables used by the desktop frontend
MULLVAD_PATH- Allows changing the path to the folder with themullvad-problem-reporttool
when running in development mode. Defaults to:<repo>/target/debug/.MULLVAD_DISABLE_UPDATE_NOTIFICATION- If set to1, notification will be disabled when
an update is available.
Command line tools for Electron app development
$ npm run develop- develop app with live-reload enabled$ npm run lint- lint code$ npm run pack:<OS>- prepare app for distribution for your platform. Where<OS>can belinux,macorwin$ npm test- run tests
Tray icon on Linux
The requirements for displaying a tray icon vary between different desktop environments. If the
tray icon does not appear, try one of the following methods:
GNOME
If you're using GNOME, you might have to install additional GNOME shell extensions to display the tray icon properly.
We recommend AppIndicator and KStatusNotifierItem Support. It can be installed via GNOME's extension website:
https://extensions.gnome.org/extension/615/appindicator-support/
Other desktop environments
Try installing one of these packages using the system's package manager:
libappindicator3-1libappindicator1libappindicator
Repository structure
Electron app and electron-builder packaging assets
- desktop/packages/mullvad-vpn/
- assets/ - Graphical assets and stylesheets
- src/
- main/
- index.ts - Entry file for the main process
- renderer/
- app.tsx - Entry file for the renderer process
- routes.tsx - Routes configurator
- transitions.ts - Transition rules between views
- main/
- tasks/ - Gulp tasks used to build app and watch for changes during development
- distribution.js - Configuration for
electron-builder
- distribution.js - Configuration for
- test/ - Electron GUI tests
- dist-assets/ - Icons, binaries and other files used when creating the distributables
- binaries/ - Git submodule containing binaries bundled with the app. See the README
in the submodule for details - linux/ - Scripts and configuration files for the deb and rpm artifacts
- pkg-scripts/ - Scripts bundled with and executed by the macOS pkg installer
- windows/ - Windows NSIS installer configuration and assets
- binaries/ - Git submodule containing binaries bundled with the app. See the README
Building, testing and misc
- build-windows-modules.sh - Compiles the C++ libraries needed on Windows
- build.sh - Sanity checks the working directory state and then builds installers for the app
Mullvad Daemon
The daemon is implemented in Rust and is implemented in several crates. The main, or top level,
crate that builds the final daemon binary is mullvad-daemon which then depend on the others.
In general one can look at the daemon as split into two parts, the crates starting with talpid
and the crates starting with mullvad. The talpid crates are supposed to be completely unrelated
to Mullvad specific things. A talpid crate is not allowed to know anything about the API through
which the daemon fetch Mullvad account details or download VPN server lists for example. Thetalpid components should be viewed as a generic VPN client with extra privacy and anonymity
preserving features. The crates having mullvad in their name on the other hand make use of thetalpid components to build a secure and Mullvad specific VPN client.
- Cargo.toml - Main Rust workspace definition. See this file for which folders here are daemon
Rust crates. - mullvad-daemon/ - Main Rust crate building the daemon binary.
- talpid-core/ - Main crate of the VPN client implementation itself. Completely Mullvad agnostic
privacy preserving VPN client library.
Vocabulary
Explanations for some common words used in the documentation and code in this repository.
- App - This entire product (everything in this repository) is the "Mullvad VPN App", or App for
short.- Daemon - Refers to the
mullvad-daemonRust program. This headless program exposes a
management interface that can be used to control the daemon - Frontend - Term used for any program or component that connects to the daemon management
interface and allows a user to control the daemon.- GUI - The Electron + React program that is a graphical frontend for the Mullvad VPN App.
- CLI - The Rust program named
mullvadthat is a terminal based frontend for the Mullvad
VPN app.
- Daemon - Refers to the
File paths used by Mullvad VPN app
A list of file paths written to and read from by the various components of the Mullvad VPN app
Daemon
On Windows, when a process runs as a system service the variable %LOCALAPPDATA% expands toC:\Windows\system32\config\systemprofile\AppData\Local.
All directory paths are defined in, and fetched from, the mullvad-paths crate.
Settings
The settings directory can be changed by setting the MULLVAD_SETTINGS_DIR environment variable.
| Platform | Path |
|---|---|
| Linux | /etc/mullvad-vpn/ |
| macOS | /etc/mullvad-vpn/ |
| Windows | %LOCALAPPDATA%\Mullvad VPN\ |
| Android | getFilesDir() |
Logs
The log directory can be changed by setting the MULLVAD_LOG_DIR environment variable.
| Platform | Path |
|---|---|
| Linux | /var/log/mullvad-vpn/ + systemd |
| macOS | /var/log/mullvad-vpn/ |
| Windows | C:\ProgramData\Mullvad VPN\ |
| Android | getFilesDir() |
Cache
The cache directory can be changed by setting the MULLVAD_CACHE_DIR environment variable.
| Platform | Path |
|---|---|
| Linux | /var/cache/mullvad-vpn/ |
| macOS | /Library/Caches/mullvad-vpn/ |
| Windows | C:\ProgramData\Mullvad VPN\cache |
| Android | getCacheDir() |
RPC address file
The full path to the RPC address file can be changed by setting the MULLVAD_RPC_SOCKET_PATH
environment variable.
| Platform | Path |
|---|---|
| Linux | /var/run/mullvad-vpn |
| macOS | /var/run/mullvad-vpn |
| Windows | //./pipe/Mullvad VPN |
| Android | getNoBackupFilesDir() |
Desktop Electron app
The desktop Electron app has a specific settings file that is configured for each user. The path is
set in the desktop/packages/mullvad-vpn/src/main/gui-settings.ts file.
| Platform | Path |
|---|---|
| Linux | $XDG_CONFIG_HOME/Mullvad VPN/gui_settings.json |
| macOS | ~/Library/Application Support/Mullvad VPN/gui_settings.json |
| Windows | %LOCALAPPDATA%\Mullvad VPN\gui_settings.json |
| Android | Present in Android's logcat |
Icons
See graphics README for information about icons.
Locales and translations
Instructions for how to handle locales and translations are found
here.
For instructions specific to the Android app, see here.
For instructions specific to the iOS app, see here.
License
Copyright (C) 2026 Mullvad VPN AB
This program is free software: you can redistribute it and/or modify it under the terms of the
GNU General Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
For the full license agreement, see the LICENSE.md file
The source code for the iOS app is GPL-3 licensed like everything else in this repository.
But the distributed app on the Apple App Store is not GPL licensed,
it falls under the Apple App Store EULA.
File: android/docs/BuildInstructions.macos.md
Build Instructions for macOS
This document will guide you to setup your development environment on macOS. It has been
tested on a clean install of macOS Ventura 13.5.1 on a M2 MacBook.
As an alternative to this guide you can also follow these nix devshell instructions.
1. Install Prerequisites
NOTE: Following instructions assume that you have brew installed.
If you do not have Android Studio installed, install it:
brew install --cask android-studioInstall the following packages:
brew install protobuf gcc openjdk@21 rustup-init python3NOTE: Ensure that you setup
openjdk@21to be the active JDK, follow instructions in
installation of openjdk@21 or use a tool like jEnv.
Finish the install of rustup:
rustup-init2. Install SDK Tools and Android NDK Toolchain
Open Android Studio -> Tools -> SDK Manager, and install Android SDK Command-line Tools (latest).
Install the necessary Android SDK tools
~/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager "platforms;android-37" "build-tools;37.0.0" "platform-tools" "ndk;27.3.13750724"Install Android targets
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-androidExport the following environmental variables, and possibly store them for example in your~/.zprofile or ~/.zshrc file:
export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/27.3.13750724"
export NDK_TOOLCHAIN_DIR="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin"
export AR_aarch64_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar"
export AR_armv7_linux_androideabi="$NDK_TOOLCHAIN_DIR/llvm-ar"
export AR_x86_64_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar"
export AR_i686_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar"
export CC_aarch64_linux_android="$NDK_TOOLCHAIN_DIR/aarch64-linux-android26-clang"
export CC_armv7_linux_androideabi="$NDK_TOOLCHAIN_DIR/armv7a-linux-androideabi26-clang"
export CC_x86_64_linux_android="$NDK_TOOLCHAIN_DIR/x86_64-linux-android26-clang"
export CC_i686_linux_android="$NDK_TOOLCHAIN_DIR/i686-linux-android26-clang"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_TOOLCHAIN_DIR/aarch64-linux-android26-clang"
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$NDK_TOOLCHAIN_DIR/armv7a-linux-androideabi26-clang"
export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$NDK_TOOLCHAIN_DIR/i686-linux-android26-clang"
export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$NDK_TOOLCHAIN_DIR/x86_64-linux-android26-clang"3. Debug build
Android Studio
Create the file android/local.properties if it does not exist and add the following line:
rust.pythonCommand=/opt/homebrew/bin/python3You should now be able to run the app directly from Android Studio.
Command line
From the android/ directory, run the following command to assemble all the native libraries and the app:
./gradlew debugBuild options and configuration
For configuring signing or options to your build continue with the general build instructions.
Debugging the Rust native code
For tips on how to debug the Rust library code from Android Studio, see the debug instructions.
File: android/docs/DebugInstructions.md
Debugging the native Rust libraries in Android Studio with LLDB
- In
gradle.propertiesor in your$HOME/.gradle/gradle.propertiesfile set the following:
mullvad.app.build.keepDebugSymbols=true
mullvad.app.build.replaceRustPathPrefix=false- In Android Studio, go to
Run -> Edit configurations... - Make sure the
appconfiguration is selected. - In the
Debuggertab, selectDual (Java + Native) - Start debugging the app as usual from Android Studio.
- Click the
View Breakpoints...icon in the debug view. - Click
+ -> Symbolic Breakpointsand enter a function name in theSymbol namefield.
Android Studio should now break on the function you selected.
NOTE: When running LLDB, Android Studio can sometimes get into a state where it will try to
connect to the debugger when running the app normally, which blocks the app from starting.
To fix this run adb shell am clear-debug-app.
Setting line breakpoints in Rust code by clicking the gutter in Android Studio
By default, the normal debugging support in Android Studio such as clicking the gutter to set line breakpoints only
work when debugging C/C++ and not Rust code.
To fix this, install the following Android Studio plugin: https://github.com/kl/gutter-breakpoints-rust-plugin
Improving LLDB interaction with common Rust types
Rust ships with the rust-lldb shell script (https://github.com/rust-lang/rust/blob/main/src/etc/rust-lldb) which
improves the LLDB experience when debugging Rust code, for example by being able to see the text value when
inspecting a String value.
To enable this when debugging in Android Studio, first find out your rustc sysroot by runningrustc --print sysroot. Then go to Debugger -> LLDB Startup Commands and enter the following
two commands:
command script import SYSROOT_PATH/lib/rustlib/etc/lldb_lookup.py
command source SYSROOT_PATH/lib/rustlib/etc/lldb_commandsFile: android/translations-converter/README.md
Translations converter tool
A tool for helping two-way sync the translations between the Android and Desktop apps.
How to run
Run the following command (requires rust and cargo which can be installed using rustup.rs):
cargo runTranslations files
The tool creates the appropriate localized message files and directories under the
Android project resources (e.g. values-sv/strings.xml)
for each locale it can find in the desktop/packages/mullvad-vpn/locales directory. It will also update the
messages.pot template file with the string messages from the Android app for which it did not find
any translation, making it simpler to use the template for obtaining those translations.
File: android/README.md
Mullvad VPN Android app
This directory contains the Android project as well as related files and information. Apart from the
content of this directory, the Android project also relies on building the mullvad-daemon
(which includes the GotaTun WireGuard implementation) as a shared library bundled into the assembled APK.
The app is available for download on
our website,
GitHub Releases,
F-Droid and
Google Play.
Quick start
Browsing the source code
The content in this directory (<repository-root>/android) follows a standard Android project
structure and can therefore be opened in Android Studio or any other IDE or editor of your choice.
Building the app
The easiest and recommended way to build the Android project is to use the containerized build
(which requires podman):
../building/containerized-build.sh androidSee the build instructions for further information.
Linting and formatting
Kotlin formatting
ktfmt is used for kotlin formatting.
See the official documentation for how to use it as default
formatter in Android Studio. Ensure to set kotlinLangStyle as "Code Style" and to set the project
to rely on the EditorConfig (.editorconfig file).
Also, see the ktfmt gradle plugin documentation for
how to use it as a gradle task.
XML formatting
In order to format XML files, the script scripts/tidy.sh is used. As the script name implies, it's basically a helper script to run the tool called tidy. It needs to be installed unless the
container image is used.
Command to format:
scripts/tidy.sh formatCommand to format and check for any changes:
scripts/tidy.sh formatAndCheckDiffmacOS
Since macOS is using a different version of sed running the tidy script (scripts/tidy.sh) will
lead to the creation of a large number of files ending with -e. The recommended fix for this
issue is to install the gnu version of sed. This can be done by running:brew install gnu-sed and then set gnu-sed as your default sed.
Android Gradle Plugin lint tool
The Android Gradle Plugin lint tool for linting of resources and code. The tool is configured to be
strict. See each build.gradle.ktsfor more information.
Translations and localization
See the locale README for how to easily update translations. It also
includes documentation for which phrases and terms shouldn't be translated (Do Not Translate).
Also see the translations converter README for documentation about
the tool used to sync translations between the Android and Desktop apps.
Icons and assets
For a general overview of icons and graphics read the graphics readme.
File: audits/README.md
Audits, pentests and external security reviews
Independent audits help to discover potential security vulnerabilities and fix them, all resulting
in an even better service. It also gives you the opportunity to judge whether or not we are
technically competent enough to provide a service in which security is paramount.
We perform these external security audits every two years. Here are all the audits we have
performed on this app so far:
Additional audits and certifications
Apart from the biannual audits mentioned above, we've also conducted the following:
- 2025-02-24 - NCC Group Mobile Application Security Assessment (MASA) of the Android app
- 2025-03-20 - Audit of the installer downloader by Assured
- 2026-02-17 - Leviathan security assessment of our Android app
File: building/README.md
Mullvad VPN app build containers
Substitute ${repo} with the actual absolute path to this repository
Building and publishing a production container image
These instructions describe how to set up the trusted machine that builds, signs and publishes
the container images to ghcr.io.
If you sudo into a build account that do the builds, you need to set the permissions on the tty,
so podman can ask for the passphrase for the gpg key:
realuser@server $ sudo chown build:build $(tty)
realuser@server $ sudo -u build -iConfigure podman to store signatures when building and pushing images. ~/.config/containers/registries.d/mullvad.yaml:
docker:
ghcr.io/mullvad:
sigstore-staging: file://${repo}/building/sigstoreSign in to ghcr.io with a classic auth token. Read more here:
https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic.
The TLDR is basically:
- Go to https://github.com/settings/tokens/new?scopes=write:packages
- Create a new token named
build@app-build-linux3withwrite:packagesas the only permission.
Set a very short expiry time on the token, since it's tied to your personal account. - Log podman in to ghcr.io with the new token. :This login session is valid as long as the token is valid. You can revoke the token on githubtext
$ podman login ghcr.io -u <YOUR_PERSONAL_GITHUB_USERNAME> Password: <Here you paste the token generated above>
as soon as the container building and pushing is done.
Build and publish the container image. Tag it with the github hash of the current commit.
This also adds the container GPG signatures to the sigstore and commits that to git.
The single sigstore addition (signed) commit can be pushed directly to the main branch without PR.
# Builds a new container image, pushes it to the registry, signs it and commits the signatures in git
./build-and-publish-container-image.sh (linux|android)
# Pushes the new sigstore entry
GIT_SSH_COMMAND="ssh -i ~/.ssh/id_ed25519-mullvadvpn-app-deploy" git pushWhen satisfied with how the new image works, the building/{linux,android}-container-image.txt
files can be updated to point to the new image. They name it by digest:
ghcr.io/mullvad/mullvadvpn-app-build@sha256:<digest>Not by tag. A tag can be repointed at a different image, which would silently change
what everyone builds in. A digest cannot.
build-and-publish-container-image.sh prints the exact line to use when it is done. To go the
other way, from a digest to the commit the image was built from, find the signed commit that addedbuilding/sigstore/mullvad/@sha256=<digest>/. Its message names the tag.
This update is usually done in a separate PR by a developer
Building and publishing a development image container image
These instructions describe how to set up a development machine to build, sign and publish container
images. The purpose of this is mainly to verify the build-and-publish-container-image.sh
script as well as the built images.
Set the following environment variables to override the default values:
REGISTRY_HOSTREGISTRY_ORGCONTAINER_SIGNING_KEY_FINGERPRINT
Configure podman to store signatures when building and pushing images (substitute ${testorg}). ~/.config/containers/registries.d/$testorg.yaml:
docker:
ghcr.io/$testorg:
sigstore-staging: file://${repo}/building/sigstoreIn order to verify the signature of the development images, you'll also need to follow the pull and
verification steps with some slight adjustments.
Pulling, verifying and using build images
These instructions describe how anyone can pull the images and verify them with GPG before using them.
Copy the Mullvad app container signing GPG key to somewhere outside the repository (so a git pull can't overwrite it with a malicious key):
cp ${repo}/building/mullvad-app-container-signing.asc /path/to/mullvad-app-container-signing.ascConfigure a strict policy for podman when pulling from ghcr.io/mullvad. ~/.config/containers/policy.json:
{
"default": [{ "type": "insecureAcceptAnything" }],
"transports": {
"docker": {
"ghcr.io/mullvad": [
{
"type": "signedBy",
"keyType": "GPGKeys",
"keyPath": "/path/to/mullvad-app-container-signing.asc"
}
]
}
}
}Configure podman to fetch image signatures from the in-repo sigstore directory. ~/.config/containers/registries.d/mullvad.yaml:
docker:
ghcr.io/mullvad:
sigstore: file://${repo}/building/sigstoreFile: ci/ios/create-vm/README.md
Creating new macOS VMs to build MullvadVPN iOS in a CI environment
This guide assumes you are running on macOS.
Prerequisites
In order to create VMs on the fly, we decided to use tart and packer.
The various scripts that run in the VM are written in bash with the help of shellcheck.
VM requirements
- You will need at least 60GB of available space on your VM host
- You will need at least 8GB of available RAM on your VM host
- You will need at least 4 CPU cores available on your VM host
How to install Tart
- brew install
cirruslabs/cli/tart
How to install Packer
- brew tap
hashicorp/tap - brew install
hashicorp/tap/packer
How to install shellcheck
- brew install
shellcheck
[!IMPORTANT]
Prerequisite setup before running packer
- Get a copy of the Xcode version you want to install on the VM in a xip format
- Copy that file into the folder named
vm_shared_folder- Open the file named
variables.pkrvars.hcl- Edit the variables named
xcode_versionandxcode_xip_name
Here is an example of what to expect
% ls vm_shared_folder
Xcode_15.0.1.xip
% head -2 variables.pkrvars.hcl
xcode_version = "15.1"
xcode_xip_name = "Xcode_15.1.xip"Sanity checks before running packer
It is a good idea to keep logs, the logs folder is provided to that effect.
Enable packer logs by setting the following environment variables (assuming your are running with zsh)
- export
PACKER_LOG=1 - export
PACKER_LOG_PATH="logs/packer_logs.txt"
[!NOTE]
The logs will be overwritten with each packer command you issue.
You can then check that the templates are valid before running packer
- packer inspect
-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl - packer validate
-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl
You can make sure you are not missing any dependencies with the init command
- packer init
install-vanilla-ventura.pkr.hcl
Create the VM image via Packer
Once your setup is ready, you just need one command to create a VM. And one more to install Xcode on it.
- packer build
-var-file="variables.pkrvars.hcl" install-vanilla-ventura.pkr.hcl
Install Xcode on the VM image via Packer
- packer build
-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl
[!IMPORTANT]
At the time of writing this,tartdoes not support VM snapshotting. This means that any action taken by packer will be permanent on the VM.
Make sure to properly clean up the VM before running packer commands again if something went wrong.
You can look at the cleanup.sh script in the scripts folder to see what type of cleanup is ran in case things go wrong.
File: ci/ios/test-router/README.md
Router setup
Installing on a new router/computer
Obtain an x86 computer with 2 ethernet interfaces.
Install NixOS on the hardware following the NixOS installation guide
Copy the generated
/etc/nixos/hardware-config.nixfile to the flake repo, add it to git.Create a new Stagemole account, and add plentry of time to it
- Go to stagemole.eu, log in with the account number and generate a wireguard configuration with a wg
private key and wg interface IP addresses.
- Go to stagemole.eu, log in with the account number and generate a wireguard configuration with a wg
Add a new nixosConfiguration entry in
flake.nix, followingapp-team-ios-labas an example, making sure to import
the hardware config.- Be sure to include the
hardware-config.nixfile as it contains the mount config for the partitions.- Set the appropriate args for the
./router-config.niximport, as to not clash with existing SSIDs.
Also set thewgIpv4andwgIpv6args to the IP addresses from the wireguard config.
- Set the appropriate args for the
- Be sure to include the
Apply the new configuration either via SSH or by copying the flake over to the nix machine
nixos-reubild switch .#$newMachine --target-host root@$newMachine-ipif one can SSH into the machinenixos-reubild switch .$pathToFlake#$newMachineif flake is copied to nix machine, with$pathToFlakebeing the
path to this flake directory.
Copy the wireguard private key from the generated config to the file
/staging-wg-private-key
Livebooting
One can create an ISO to live-boot a router needing to permanently install this config. There are two drawbacks:
- Still need to know the MAC addresses of the interfaces upfront.
- Any updates to the running system will not persist.
To do this, add a nixosConfiguration with an extra import of the installer ISO profile like so:
nixosConfigurations.app-team-ios-lab-iso = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
(import ./router-config.nix {
ssid = "app-team-ios-tests";
lanMac = "48:21:0b:36:bb:52";
wanMac = "48:21:0b:36:43:a3";
lanIp = "192.168.105.1/24";
wgIpv4 = "10.64.9.184/32";
wgIpv6 = "fc00:bbbb:bbbb:bb01::a40:9b8/128";
})
"${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix"
{
isoImage.squashfsCompression = "lz4";
}
];
};And build it like so:nix build .#nixosConfigurations.app-team-ios-lab-iso.config.system.build.isoImage
Quirks & features
- Since Apple doesn't allow access to LAN without the user accepting a privacy
dialog, TCP connections to8.8.8.8:80are NAT'ed to the gateway address.
File: ci/ios/upload-vm/README.md
Gemfiles for uploading to Appstore Connect.
These gemfiles specify the fastlane dependencies needed to run our script that uploads our iOS app
to the AppStore. This should be done once when setting up the upload VM.
To set up fastlane, you should invoke bundle like so:
bundle installTo run fastlane, one should use:
bundle exec fastlane $fastlane_commandTo update fastlane dependencies, one should run bundle update on a local machine, verify that the
changes are sound, and update the upload VM accordingly if they are.