Docker firewall rules conflicting with ipv6 KVM bridge network

Okay, this is a question + solution in case others run into the issue. I had an infuriating problem where libvirt guest KVM VMs couldn’t assign a real ipv6 address (only link local). I struggled to track it down because I don’t believe I was flushing all iptables rules consistently so the issue was intermittent (I’ve now switched to nftables where that is simpler). A useful diagnostic tool was the rdisc6 command from nixpkgs#ndisc6 which indicated that router solicitations failed with the docker rules enabled:

From guest:

sudo rdisc6 -1 <ethernet interface>

resulting in “timed out” messages.

It turns out this is a common issue with docker where it blocks forwarding outside of its own bridge interface. The main workaround on the arch wiki where docker is set to use this bridge wasn’t an option because I’m using the bridge as my host interface (so it has IPs assigned from an upstream router).

Anyway, the workaround with nftables:

networking.nftables.enable = true;
networking.firewall = {
    filterForward = true;
    extraForwardRules = "iifname br0 accept";
  };

where br0 is the host bridge interface.

Why this only affected ipv6 and not ipv4 I cannot say, given the other discussions I found indicated that both should be broken.

EDIT: One issue is that if docker is restarted its own rules seem to take priority again until I also restart nftables.

EDIT 2: Okay, the workaround above doesn’t solve the problem. Restarting nftables clears all of the iptables compatibility rules created by docker. I’m not sure if my workaround is incorrect or if the docker created rules are taking priority over the nft rule.

Here’s a new, kind of dumb workaround. Instead of adding a rule to nixos-fw forward-accept chain, I’ve instead created a new table and chain that matches the names that docker uses:

networking.nftables.tables = {
    filter = {
      content = ''
  chain FORWARD {
    type filter hook forward priority 0;
    iifname br0 accept
  }
  '';
      family = "ip6";
    };
  };

So when docker runs or is restarted, it adds its own rules to the table I’ve created, leaving the rule I created alone. I also tried creating my own table with a unique name in a similar way to above but even when messing with priority I couldn’t get the chain I created to take priority over the chain that docker creates.

Only issue with above is that I couldn’t get it to create a rule with ipv4 in the same manner since the tables that docker creates both have the name “filter” just with different inet, and so nix complains about duplicate “content”. Not a big deal since I’m not experiencing any issues currently with ipv4.