mullvadvpn-app Documentation

The Mullvad VPN client app for desktop and mobile

7,441 stars Rust Rust 1 file ยท ~10,478 tokens #electron#nodejs#openvpn#rust#vpn#wireguard
RAW Doc

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 the
verify-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:

bash
git clone https://github.com/mullvad/mullvadvpn-app.git
cd mullvadvpn-app
git submodule update --init

We 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 pflog0 interface.
      • 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.
  • TALPID_FIREWALL_DONT_SET_SRC_VALID_MARK - Set this variable to 1 to stop the daemon from
    setting the net.ipv4.conf.all.src_valid_mark kernel parameter to 1 on 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. If rp_filter is set to 1 on the interface
    that will be receiving relay traffic, and src_valid_mark is not set to 1, the daemon will
    not be able to receive relay traffic.

  • TALPID_FIREWALL_DONT_SET_ARP_IGNORE - Set this variable to 1 to stop the daemon from
    setting the net.ipv4.conf.all.arp_ignore kernel parameter to 2 on 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.conf file directly
      • "resolvconf": use the resolvconf program
      • "systemd": use systemd's resolved service through DBus
      • "network-manager": use NetworkManager service through DBus
    • Windows

      • iphlpapi: use the IP helper API
      • tcpip: set TCP/IP parameters in the registry
  • TALPID_DISABLE_LOCAL_DNS_RESOLVER - Set this variable to 1 to disable the local DNS resolver
    (macOS only).

  • TALPID_NEVER_FILTER_AAAA_QUERIES - Set this variable to 1 to 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 the net_cls controller 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 to daemon.log. By default, this is disabled in
    release-builds and enabled in debug-builds. Set variable to 1 or 0 to 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:

bat
setx TALPID_DISABLE_OFFLINE 1 /m

For the change to take effect, restart the daemon:

bat
sc.exe stop mullvadvpn
sc.exe start mullvadvpn

Linux

Edit the systemd unit file via systemctl edit mullvad-daemon.service:

ini
[Service]
Environment="TALPID_DISABLE_OFFLINE_MONITOR=1"

For the change to take effect, restart the daemon:

bash
sudo systemctl restart mullvad-daemon

macOS

Use plutil:

bash
sudo plutil -replace EnvironmentVariables -json '{"TALPID_DISABLE_OFFLINE_MONITOR": "1"}' /Library/LaunchDaemons/net.mullvad.daemon.plist

For the change to take effect, restart the daemon:

bash
launchctl unload -w /Library/LaunchDaemons/net.mullvad.daemon.plist
launchctl load -w /Library/LaunchDaemons/net.mullvad.daemon.plist

Environment variables used by the desktop frontend

  • MULLVAD_PATH - Allows changing the path to the folder with the mullvad-problem-report tool
    when running in development mode. Defaults to: <repo>/target/debug/.
  • MULLVAD_DISABLE_UPDATE_NOTIFICATION - If set to 1, 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 be
    linux, mac or win
  • $ 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-1
  • libappindicator1
  • libappindicator

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
    • tasks/ - Gulp tasks used to build app and watch for changes during development
      • distribution.js - Configuration for electron-builder
    • 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

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. The
talpid 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 the
talpid 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-daemon Rust 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 mullvad that is a terminal based frontend for the Mullvad
        VPN app.

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 to
C:\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:

bash
brew install --cask android-studio

Install the following packages:

bash
brew install protobuf gcc openjdk@21 rustup-init python3

NOTE: Ensure that you setup openjdk@21 to be the active JDK, follow instructions in
installation of openjdk@21 or use a tool like jEnv.

Finish the install of rustup:

bash
rustup-init

2. 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

bash
~/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

bash
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android

Export the following environmental variables, and possibly store them for example in your
~/.zprofile or ~/.zshrc file:

bash
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:

bash
rust.pythonCommand=/opt/homebrew/bin/python3

You 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:

bash
./gradlew debug

Build 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

  1. In gradle.properties or in your $HOME/.gradle/gradle.properties file set the following:
text
mullvad.app.build.keepDebugSymbols=true
mullvad.app.build.replaceRustPathPrefix=false
  1. In Android Studio, go to Run -> Edit configurations...
  2. Make sure the app configuration is selected.
  3. In the Debugger tab, select Dual (Java + Native)
  4. Start debugging the app as usual from Android Studio.
  5. Click the View Breakpoints... icon in the debug view.
  6. Click + -> Symbolic Breakpoints and enter a function name in the Symbol name field.

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 running
rustc --print sysroot. Then go to Debugger -> LLDB Startup Commands and enter the following
two commands:

text
command script import SYSROOT_PATH/lib/rustlib/etc/lldb_lookup.py
command source SYSROOT_PATH/lib/rustlib/etc/lldb_commands

File: 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):

bash
cargo run

Translations 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):

text
../building/containerized-build.sh android

See 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:

text
scripts/tidy.sh format

Command to format and check for any changes:

text
scripts/tidy.sh formatAndCheckDiff

macOS

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:


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:

text
realuser@server $ sudo chown build:build $(tty)
realuser@server $ sudo -u build -i

Configure podman to store signatures when building and pushing images. ~/.config/containers/registries.d/mullvad.yaml:

yml
docker:
  ghcr.io/mullvad:
    sigstore-staging: file://${repo}/building/sigstore

Sign 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:

  1. Go to https://github.com/settings/tokens/new?scopes=write:packages
  2. Create a new token named build@app-build-linux3 with write:packages as the only permission.
    Set a very short expiry time on the token, since it's tied to your personal account.
  3. Log podman in to ghcr.io with the new token. :
    text
    $ podman login ghcr.io -u <YOUR_PERSONAL_GITHUB_USERNAME>
    Password: <Here you paste the token generated above>
    This login session is valid as long as the token is valid. You can revoke the token on github
    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.

text
# 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 push

When 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:

text
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 added
building/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_HOST
  • REGISTRY_ORG
  • CONTAINER_SIGNING_KEY_FINGERPRINT

Configure podman to store signatures when building and pushing images (substitute ${testorg}). ~/.config/containers/registries.d/$testorg.yaml:

yml
docker:
  ghcr.io/$testorg:
    sigstore-staging: file://${repo}/building/sigstore

In 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):

text
cp ${repo}/building/mullvad-app-container-signing.asc /path/to/mullvad-app-container-signing.asc

Configure a strict policy for podman when pulling from ghcr.io/mullvad. ~/.config/containers/policy.json:

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:

yml
docker:
  ghcr.io/mullvad:
    sigstore: file://${repo}/building/sigstore

File: 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_version and xcode_xip_name

Here is an example of what to expect

bash
% 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, tart does 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.nix file 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.
  • Add a new nixosConfiguration entry in flake.nix, following app-team-ios-lab as an example, making sure to import
    the hardware config.

    • Be sure to include the hardware-config.nix file as it contains the mount config for the partitions.
      • Set the appropriate args for the ./router-config.nix import, as to not clash with existing SSIDs.
        Also set the wgIpv4 and wgIpv6 args to the IP addresses from the wireguard config.
  • 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-ip if one can SSH into the machine
    • nixos-reubild switch .$pathToFlake#$newMachine if flake is copied to nix machine, with $pathToFlake being 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:

nix
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 to 8.8.8.8:80 are 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:

bash
bundle install

To run fastlane, one should use:

bash
bundle exec fastlane $fastlane_command

To 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.