## 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](https://mullvad.net) (Also accessible via Tor on our
[onion service](http://o54hon2e2vj6c7m3aqqu6uyece65by3vgoxxhlqlsvkmacw6a7m7kiad.onion/)).

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`](mullvad-daemon/)), a graphical user interface ([GUI](desktop/)) and a command
line interface ([CLI](mullvad-cli/)). The Android app uses the same backing system service for the
tunnel and security but has a dedicated frontend in [android/](android/). iOS consists of a
completely standalone implementation that resides in [ios/](ios/).

## Releases

There are built and signed releases for macOS, Windows, Linux and Android available on
[our website](https://mullvad.net/download/) and on
[GitHub](https://github.com/mullvad/mullvadvpn-app/releases/). The Android app is also available
on [Google Play] and [F-Droid] and the iOS version on [App Store].

[Google Play]: https://play.google.com/store/apps/details?id=net.mullvad.mullvadvpn
[F-Droid]: https://f-droid.org/packages/net.mullvad.mullvadvpn/
[App Store]: https://apps.apple.com/us/app/mullvad-vpn/id1488466513

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](docs/supported-platforms.md) 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](./audits)          |    ✓    |   ✓   |   ✓   |    ✓    |  ✓ |

\* The local network is always accessible on iOS with the current implementation

[DAITA]: https://mullvad.net/en/blog/introducing-defense-against-ai-guided-traffic-analysis-daita

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

[dedicated security document]: docs/security.md

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

[](https://www.bestpractices.dev/projects/9411)

### 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`](.github/workflows/verify-locked-down-signatures.yml)
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](./audits/README.md) for this.

Moreover, we welcome any individual to review the security of this app and submit any found
issue to us. See [SECURITY.md](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](https://github.com/mullvad/mullvadvpn-app-binaries) README for more
details about that repository.

## Building the app

See the [build instructions](BuildInstructions.md) for help building the app on desktop platforms.

For building the Android app, see the [instructions](./android/docs/BuildInstructions.md) for Android.

For building the iOS app, see the [instructions](./ios/BuildInstructions.md) for iOS.

## Releasing the app

See [this](Release.md) 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()`](https://developer.android.com/reference/android/content/Context#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()`](https://developer.android.com/reference/android/content/Context#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()`](https://developer.android.com/reference/android/content/Context#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()`](https://developer.android.com/reference/android/content/ContextWrapper#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](graphics/README.md) for information about icons.

## Locales and translations

Instructions for how to handle locales and translations are found
[here](./desktop/packages/mullvad-vpn/locales/README.md).

For instructions specific to the Android app, see [here](./android/README.md).

For instructions specific to the iOS app, see [here](./ios/translation/README.md).

# 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].

[Apple App Store EULA]: https://www.apple.com/legal/internet-services/itunes/dev/stdeula/
[Mullvad's Open Source page]: https://mullvad.net/en/guides/open-source/

---

## 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](BuildInstructions.md#build-using-nix-devshell).

## 1. Install Prerequisites

> __*NOTE:*__ Following instructions assume that you have [brew](https://brew.sh/) 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](https://www.jenv.be/).

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](BuildInstructions.md).

# Debugging the Rust native code

For tips on how to debug the Rust library code from Android Studio, see the [debug instructions](DebugInstructions.md).

---

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

```
mullvad.app.build.keepDebugSymbols=true
mullvad.app.build.replaceRustPathPrefix=false
```

2. In Android Studio, go to `Run -> Edit configurations...`
3. Make sure the `app` configuration is selected.
4. In the `Debugger` tab, select `Dual (Java + Native)`
5. Start debugging the app as usual from Android Studio.
6. Click the `View Breakpoints...` icon in the debug view.
7. 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:

```
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](https://rustup.rs/)):
```bash
cargo run
```

## Translations files

The tool creates the appropriate localized message files and directories under the
[Android project resources][android-resources] (e.g. [values-sv/strings.xml][values-sv-example])
for each locale it can find in the [`desktop/packages/mullvad-vpn/locales` directory][desktop-locales]. 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.

[android-resources]: ../lib/ui/resource/src/main/res/
[desktop-locales]: ../../desktop/packages/mullvad-vpn/locales/
[messages.pot]: ../../desktop/packages/mullvad-vpn/locales/messages.pot
[values-sv-example]: ../lib/ui/resource/src/main/res/values-sv/strings.xml

---

## 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](../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](https://mullvad.net/download/android/),
[GitHub Releases](https://github.com/mullvad/mullvadvpn-app/releases),
[F-Droid](https://f-droid.org/packages/net.mullvad.mullvadvpn/) and
[Google Play](https://play.google.com/store/apps/details?id=net.mullvad.mullvadvpn).

## 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 android
```
See the [build instructions](./docs/BuildInstructions.md) for further information.

## Linting and formatting

### Kotlin formatting
`ktfmt` is used for kotlin formatting.

See the [official documentation](https://github.com/facebook/ktfmt) 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](https://github.com/cortinico/ktfmt-gradle) 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 format
```

Command to format and check for any changes:
```
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.kts`for more information.

## Translations and localization

See the [locale README][desktop-locales-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][translations-converter-readme] for documentation about
the tool used to sync translations between the Android and Desktop apps.

[desktop-locales-readme]: ../desktop/packages/mullvad-vpn/locales/README.md
[translations-converter-readme]: ./translations-converter/README.md

## Icons and assets

For a general overview of icons and graphics read [the graphics readme](../graphics/README.md).

---

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

* [2018-09-24 - Assured and Cure53](./2018-09-24-assured-cure53.md)
* [2020-06-12 - Cure53](./2020-06-12-cure53.md)
* [2022-10-14 - Atredis](./2022-10-14-atredis.md)
* [2024-12-10 - X41 D-Sec](./2024-12-10-X41-D-Sec.md)

## 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-02-24-nccgroup-android-masa.md)
* [2025-03-20 - Audit of the installer downloader by Assured](./2025-03-20-assured-installer-downloader.md)
* [2026-02-17 - Leviathan security assessment of our Android app](./2026-02-17-leviathan-masa.md)

---

## 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 -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. :
   ```
   $ 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.
```
# 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:

```
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](#pulling-verifying-and-using-build-images) 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.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](https://tart.run/) and [packer](https://developer.hashicorp.com/packer).

The various scripts that run in the VM are written in bash with the help of [shellcheck](shellcheck.net).

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


[NixOS installation guide]: https://nixos.org/manual/nixos/stable/#sec-installation-graphical

---

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