Jump to content

Mullvad VPN

From NixOS Wiki

Mullvad VPN is a virtual private network service operated in Sweden by Mullvad VPN AB. It uses WireGuard under the hood and includes both a CLI and a GUI package.

Installation

To install Mullvad VPN, you need to enable it in your system options:

Warning: Mullvad VPN currently only works if systemd-resolved is enable. More info at this forum post. If this issue is fixed please remove this warning banner.
configuration.nix
{
  services.mullvad-vpn.enable = true;
}

If you want to use the GUI application:

configuration.nix
{ pkgs, ... }:
{
  services.mullvad-vpn.package = pkgs.mullvad-vpn;
}

Configuration

You can declaratively configure the Mullvad daemon by writing the settings.json file to the location set by the MULLVAD_SETTINGS_DIR environment variable. (/etc/mullvad-vpn by default)

The example below sets up a few custom lists that let you select a whole continent as an exit location, or simply randomizes all countries if you don't care where your IP is coming from. A caveat with this configuration is that it must be manually updated every time Mullvad implements a server in a new country.

configuration.nix
{ pkgs, ... }:
let
  mullvadConfig =
    let
      mkCountryList = id: countries: name: {
        inherit id name;
        locations = map (country: { inherit country; }) countries;
      };
    in
    pkgs.writeText "mullvad-settings" (
      builtins.toJSON {
        allow_lan = true;
        auto_connect = true;
        block_when_disconnected = true;

        api_access_methods = {
          custom = [ ];
          direct = {
            access_method.built_in = "direct";
            enabled = true;
            id = "00000000-0000-0000-0000-000000000008";
            name = "Direct";
          };
          mullvad_bridges = {
            access_method.built_in = "bridge";
            enabled = true;
            id = "00000000-0000-0000-0000-000000000009";
            name = "Mullvad Bridges";
          };
          encrypted_dns_proxy = {
            access_method.built_in = "encrypted_dns_proxy";
            enabled = true;
            id = "00000000-0000-0000-0000-000000000010";
            name = "Encrypted DNS proxy";
          };
        };

        # The bridge options are only useful for OpenVPN. You can configure Wireguard under Shadowsocks with `obfuscation_settings` below.
        bridge_state = "auto";
        bridge_settings = {
          bridge_type = "normal";
          custom = null;
          normal = {
            location = "any";
            ownership = "any";
            providers = "any";
          };
        };

        custom_lists.custom_lists =
          let
            northAmerica = [
              "ca"
              "mx"
              "us"
            ];
            southAmerica = [
              "br"
              "cl"
              "co"
              "pe"
            ];
            europe = [
              "al"
              "at"
              "be"
              "bg"
              "ch"
              "cy"
              "cz"
              "de"
              "dk"
              "ee"
              "es"
              "fi"
              "fr"
              "gb"
              "gr"
              "hr"
              "hu"
              "ie"
              "it"
              "lv"
              "nl"
              "no"
              "pl"
              "pt"
              "ro"
              "rs"
              "se"
              "si"
              "sk"
              "tr"
              "ua"
            ];
            africa = [ "za" ];
            asia = [
              "hk"
              "id"
              "il"
              "jp"
              "sg"
              "th"
            ];
            oceania = [
              "au"
              "nz"
            ];
          in
          [
            (mkCountryList "00000000-0000-0000-0000-000000000000" (
              northAmerica ++ southAmerica ++ europe ++ africa ++ asia ++ oceania
            ) "All Countries")
            (mkCountryList "00000000-0000-0000-0000-000000000002" northAmerica "North America")
            (mkCountryList "00000000-0000-0000-0000-000000000003" southAmerica "South America")
            (mkCountryList "00000000-0000-0000-0000-000000000004" europe "Europe")
            (mkCountryList "00000000-0000-0000-0000-000000000005" africa "Africa")
            (mkCountryList "00000000-0000-0000-0000-000000000006" asia "Asia")
            (mkCountryList "00000000-0000-0000-0000-000000000007" oceania "Oceania")
          ];

        # This is where you would configure IP overrides for Mullvad's servers, if you have any.
        relay_overrides = [
          /*
            {
              hostname = "se-sto-wg-001";
              ipv4_addr_in = "[remote IPv4]";
              ipv6_addr_in = "[remote IPv6]";
            }
          */
        ];
        relay_settings = {
          normal = {
            location.only.custom_list.list_id = "00000000-0000-0000-0000-000000000000"; # This selects all countries by default, so you get a random exit country every time you connect to your VPN.
            openvpn_constraints.port = "any";
            ownership = "any";
            providers = "any";
            tunnel_protocol = "any";
            wireguard_constraints = {
              entry_location.only.custom_list.list_id = "00000000-0000-0000-0000-000000000000"; # This will be ignored if DAITA is enabled, or if `use_multihop` is set to false.
              ip_version = "any";
              port = "any";
              use_multihop = true;
            };
          };
        };

        obfuscation_settings = {
          selected_obfuscation = "auto"; # This can be set to `shadowsocks`, for instance.
          udp2tcp.port = "any";
        };

        tunnel_options = {
          openvpn.mssfix = null;
          generic.enable_ipv6 = true;
          wireguard = {
            mtu = null;
            quantum_resistant = "auto";
            rotation_interval = null;
            # DAITA (https://mullvad.net/en/vpn/daita) can be configured here.
            daita = {
              enabled = true;
              use_multihop_if_necessary = true;
            };
          };
          dns_options = {
            state = "default";
            custom_options.addresses = [ ]; # Configure custom DNS providers here.
            default_options = {
              # Configure your DNS blocking. (Only usable if the option above is an empty list.)
              block_ads = true;
              block_trackers = true;
              block_malware = true;
              block_g*mbling = true; # Yes, this is exactly what you think it is. The Wiki's protection filter blocks this word. TODO: Someone in the trusted group needs to add the word.
              block_adult_content = true;
              block_social_media = true;
            };
          };
        };

        settings_version = 10; # This configuration is up to date as of Mullvad VPN 2025.2. Usually, Mullvad will automatically migrate your configuration imperatively, but you may occasionally need to edit it here.
        show_beta_releases = false;
      }
    );
in
{
  systemd = {
    services."mullvad-daemon".environment.MULLVAD_SETTINGS_DIR = "/var/lib/mullvad-vpn"; # This is necessary if `system.etc.overlay.mutable` is set to false, because Mullvad expects the settings directory to be writable.
    tmpfiles.settings."10-mullvad-settings"."/var/lib/mullvad-vpn/settings.json"."C+" = {
      group = "root";
      mode = "0700"; # This isn't necessary for the settings file itself, but Mullvad will store your WireGuard private key on the same directory as the settings file, so ensure that /var/lib/mullvad-vpn or /etc/mullvad-vpn is owned by `root` and has -rwx------ permissions.
      user = "root";
      argument = "${mullvadConfig}";
    };
  };
}

Autostarting the GUI application

If you don't want to rely on Mullvad's autostart file in ~/.config/autostart, (Perhaps because your configuration is stateless) you can set up an autostart file with makeAutostartItem:

configuration.nix
{ pkgs, ... }:
let
  mullvad-autostart = pkgs.makeAutostartItem {
    name = "mullvad-vpn";
    package = pkgs.mullvad-vpn;
  };
in
{
  environment.systemPackages = [ mullvad-autostart ];
}