Skip to content

nixos/mullvad-vpn: Can't apply nftables exclude ruleset to allow local network connections #383466

Description

@Andrew15-5

Nixpkgs version

  • Unstable (25.05)

Describe the bug

On Pop!_OS I was using

define EXCLUDED_IPS = {
  192.168.0.0/16
}

table inet MullvadExcludeTraffic {
  chain excludeOutgoing {
    type route hook output priority 0; policy accept;
    ip daddr $EXCLUDED_IPS ct mark set 0x00000f41 meta mark set 0x6d6f6c65;
  }
}

from https://mullvad.net/en/help/split-tunneling-with-linux-advanced to allow local network communications. But now with networking.firewall.enable = true; on NixOS it doesn't work. I didn't speak iptables, and I sure don't speak nftables.

full ruleset
table inet nixos-fw {
  set temp-ports {
    type inet_proto . inet_service
    flags interval
    auto-merge
    comment "Temporarily opened ports"
  }

  chain rpfilter {
    type filter hook prerouting priority mangle + 10; policy drop;
    meta nfproto ipv4 udp sport . udp dport { 68 . 67, 67 . 68 } accept comment "DHCPv4 client/server"
    fib saddr . mark . iif oif exists accept
    jump rpfilter-allow
  }

  chain rpfilter-allow {
  }

  chain input {
    type filter hook input priority filter; policy drop;
    iifname "lo" accept comment "trusted interfaces"
    ct state vmap { invalid : drop, established : accept, related : accept, new : jump input-allow, untracked : jump input-allow }
    tcp flags & (fin | syn | rst | ack) == syn log prefix "refused connection: " level info
  }

  chain input-allow {
    udp dport 5353 accept
    meta l4proto . th dport @temp-ports accept
    icmp type echo-request accept comment "allow ping"
    icmpv6 type != { nd-redirect, 139 } accept comment "Accept all ICMPv6 messages except redirects and node information queries (type 139).  See RFC 4890, section 4.4."
    ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client"
  }
}
table inet MullvadExcludeTraffic {
  chain excludeOutgoing {
    type route hook output priority filter; policy accept;
    ip daddr 192.168.0.0/16 ct mark set 0x00000f41 meta mark set 0x6d6f6c65
  }
}
table inet mangle {
  chain output {
    type route hook output priority mangle; policy accept;
    meta l4proto != tcp ct state related,new queue flags bypass to 0
    tcp flags & (fin | syn | rst | ack) == syn queue flags bypass to 0
  }
}
table inet filter {
  chain input {
    type filter hook input priority filter; policy accept;
    udp sport 53 queue flags bypass to 0
  }
}
table inet mullvad {
  chain prerouting {
    type filter hook prerouting priority -199; policy accept;
    iif != "wg0-mullvad" ct mark 0x00000f41 meta mark set 0x6d6f6c65
    ip saddr 149.88.104.2 udp sport 41113 meta mark set 0x6d6f6c65
  }

  chain output {
    type filter hook output priority filter; policy drop;
    oif "lo" accept
    ct mark 0x00000f41 accept
    udp sport 68 ip daddr 255.255.255.255 udp dport 67 accept
    ip6 saddr fe80::/10 udp sport 546 ip6 daddr ff02::1:2 udp dport 547 accept
    ip6 saddr fe80::/10 udp sport 546 ip6 daddr ff05::1:3 udp dport 547 accept
    ip6 daddr ff02::2 icmpv6 type nd-router-solicit icmpv6 code 0 accept
    ip6 daddr ff02::1:ff00:0/104 icmpv6 type nd-neighbor-solicit icmpv6 code 0 accept
    ip6 daddr fe80::/10 icmpv6 type nd-neighbor-solicit icmpv6 code 0 accept
    ip6 daddr fe80::/10 icmpv6 type nd-neighbor-advert icmpv6 code 0 accept
    ip daddr 149.88.104.2 udp dport 41113 meta mark 0x6d6f6c65 accept
    oif "wg0-mullvad" udp dport 53 ip daddr 10.64.0.1 accept
    oif "wg0-mullvad" tcp dport 53 ip daddr 10.64.0.1 accept
    udp dport 53 reject
    tcp dport 53 reject with tcp reset
    oif "wg0-mullvad" accept
    reject
  }

  chain input {
    type filter hook input priority filter; policy drop;
    iif "lo" accept
    ct mark 0x00000f41 accept
    udp sport 67 udp dport 68 accept
    ip6 saddr fe80::/10 udp sport 547 ip6 daddr fe80::/10 udp dport 546 accept
    ip6 saddr fe80::/10 icmpv6 type nd-router-advert icmpv6 code 0 accept
    ip6 saddr fe80::/10 icmpv6 type nd-redirect icmpv6 code 0 accept
    ip6 saddr fe80::/10 icmpv6 type nd-neighbor-solicit icmpv6 code 0 accept
    icmpv6 type nd-neighbor-advert icmpv6 code 0 accept
    ip saddr 149.88.104.2 udp sport 41113 ct state established accept
    iif "wg0-mullvad" accept
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
    ct mark 0x00000f41 accept
    udp sport 68 ip daddr 255.255.255.255 udp dport 67 accept
    udp sport 67 udp dport 68 accept
    ip6 saddr fe80::/10 udp sport 546 ip6 daddr ff02::1:2 udp dport 547 accept
    ip6 saddr fe80::/10 udp sport 546 ip6 daddr ff05::1:3 udp dport 547 accept
    ip6 saddr fe80::/10 udp sport 547 ip6 daddr fe80::/10 udp dport 546 accept
    ip6 daddr ff02::2 icmpv6 type nd-router-solicit icmpv6 code 0 accept
    ip6 saddr fe80::/10 icmpv6 type nd-router-advert icmpv6 code 0 accept
    ip6 saddr fe80::/10 icmpv6 type nd-redirect icmpv6 code 0 accept
    ip6 daddr ff02::1:ff00:0/104 icmpv6 type nd-neighbor-solicit icmpv6 code 0 accept
    ip6 daddr fe80::/10 icmpv6 type nd-neighbor-solicit icmpv6 code 0 accept
    ip6 saddr fe80::/10 icmpv6 type nd-neighbor-solicit icmpv6 code 0 accept
    ip6 daddr fe80::/10 icmpv6 type nd-neighbor-advert icmpv6 code 0 accept
    icmpv6 type nd-neighbor-advert icmpv6 code 0 accept
    oif "wg0-mullvad" udp dport 53 ip daddr 10.64.0.1 accept
    oif "wg0-mullvad" tcp dport 53 ip daddr 10.64.0.1 accept
    udp dport 53 reject
    tcp dport 53 reject with tcp reset
    oif "wg0-mullvad" accept
    iif "wg0-mullvad" ct state established accept
    reject
  }

  chain mangle {
    type route hook output priority mangle; policy accept;
    oif "wg0-mullvad" udp dport 53 ip daddr 10.64.0.1 accept
    oif "wg0-mullvad" tcp dport 53 ip daddr 10.64.0.1 accept
    meta cgroup 5087041 ct mark set 0x00000f41 meta mark set 0x6d6f6c65
  }

  chain nat {
    type nat hook postrouting priority srcnat; policy accept;
    oif "wg0-mullvad" ct mark 0x00000f41 drop
    oif != "lo" ct mark 0x00000f41 masquerade
  }
}
config
services.mullvad-vpn.enable = true;
networking.nftables = {
  enable = true;
  ruleset = "
    define EXCLUDED_IPS = {
      192.168.0.0/16
    }

    table inet MullvadExcludeTraffic {
      chain excludeOutgoing {
        type route hook output priority 0; policy accept;
        ip daddr $EXCLUDED_IPS ct mark set 0x00000f41 meta mark set 0x6d6f6c65;
      }
    }
  ";
};

Steps to reproduce

  1. nixos-rebuild switch --use-remote-sudo
  2. connect to Mullvad VPN
  3. ping localnode

Expected behaviour

I should be able to access nodes in the local network.

Screenshots

No response

Relevant log output

Additional context

I think that an appropriate option should be added for such exclusion method and link to https://mullvad.net/en/help/split-tunneling-with-linux-advanced in the documentation. This will make the set up experience very smooth.

System metadata

  • system: "x86_64-linux"
  • host os: Linux 6.12.11, NixOS, 25.05 (Warbler), 25.05.20250129.9d3ae80
  • multi-user?: yes
  • sandbox: yes
  • version: nix-env (Nix) 2.24.12
  • nixpkgs: /nix/store/pgg4vm83q0kr4hxzcwhdgdiv2yfnh3dw-source

Notify maintainers

@arcuru @ymarkus

Note for maintainers: Please tag this issue in your pull request description. (i.e. Resolves #ISSUE.)

I assert that this issue is relevant for Nixpkgs

Is this issue important to you?

Add a 👍 reaction to issues you find important.

Metadata

Metadata

Assignees

No one assigned

    Labels

    0.kind: bugSomething is broken2.status: stalehttps://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md6.topic: nixosIssues or PRs affecting NixOS modules, or package usability issues specific to NixOS

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions