Why is IPv6 disabled upon reboot even after configuring sysctl.conf?

I'm assuming that you are using these three packages to provide the options in use: ifupdown, bridge-utils, vlan. The two later provide the commands brctl and vconfig, both obsolete, but more importantly they provide Debian-specific plugin scripts to ifupdown. While brctl is still used in these scripts, vconfig is not even used (and replaced by modern ip link commands).

The problem is caused by the fact that br0 is parent to a VLAN sub-interface that gets created by bridge-utils scripts (not by scripts from the vlan package).

The bridge-utils's ifupdown plugin scripts prevent bridge ports to participate in routing:

# ls -l /etc/network/if-pre-up.d/bridge
lrwxrwxrwx. 1 root root 29 Jan 28  2019 bridge -> /lib/bridge-utils/ifupdown.sh

which is a Debian-specific script belonging to the bridge-utils package. Here's the relevant content (sorry this is a rare package that doesn't appear to be on https://salsa.debian.org, so no link):

      if [ -f /proc/sys/net/ipv6/conf/$port/disable_ipv6 ]
      then
        echo 1 > /proc/sys/net/ipv6/conf/$port/disable_ipv6
      fi

This is a desired setting for bridge ports.

But in OP's setup the bridge interface is intended to receive an address to participate in routing, and also to be a parent interface to a VLAN sub interface itself enslaved to a bridge. That's a topology not expected by bridge-utils.

The previous script calls /lib/bridge-utils/bridge-utils.sh which includes:

create_vlan_port()
{
# port doesn't yet exist
if [ ! -e "/sys/class/net/$port" ]
then
  local dev="${port%.*}"
  # port is a vlan and the device exists?
  if [ "$port" != "$dev" ] && [ -e "/sys/class/net/$dev" ]
  then
    if [ -f /proc/sys/net/ipv6/conf/$dev/disable_ipv6 ]
    then
      echo 1 > /proc/sys/net/ipv6/conf/$dev/disable_ipv6
    fi
    ip link set "$dev" up
    ip link add link "$dev" name "$port" type vlan id "${port#*.}"
  fi
fi
}

When the sub-interface doesn't exist (because it doesn't even need to have a configuration to be created at all with this script), its parent interface gets IPv6 disabled (while the ports itself will get it disabled from the previous script) for similar reasons to the bridge case: the parent interface is supposed to carry only VLAN tagged traffic, so is prevented to interfere with any routing for example by receiving automatic IPv6 addresses. This is also usually a desired setting, but not for OP's case where the same interface is intended to carry both tagged and untagged traffic.

In OP's setup the sub-interfaces are defined in the configuration and intended to be created on the system by plugin scripts from the vlan package, but since there aren't any auto br0.5 nor auto br0.90, the interfaces were not created at the system level when bridge-utils's script checked, so it executes the # port doesn't yet exist block: creates them but disable IPv6 on their parent interfaces first. It's important here to not confuse the logical interface as seen with ifupdown with the real interface on the system, despite them having the same name in almost all setups.

Solutions

Any of the three methods below should get the intended result. I'm also suggesting a 4th method, but integration with applications like Docker wouldn't be simple.

  • work-around this by adapting to the peculiarities of the (quite obsolete) bridge-utils package: bring up the configured sub-interfaces in advance, so they exist at the system level. Then the script above won't disable IPv6 on their parent interfaces (it won't match # port doesn't yet exist). Nor scripts from the vlan package which this time created the VLAN sub-interfaces.

    auto br0.5
    iface br0.5 inet manual
            vlan-raw-device br0
    
    auto br0.90
    iface br0.90 inet manual
            vlan-raw-device br0
    

    and make sure it happens before the configuration of br5 and br90 (which is the case now). After this, only these interfaces will have IPv6 disabled, as it should be: br0.5, br0.90 as well as enp175s0f1, enp175s0f0, hostveth0.

    While this is a simple change, it won't prevent problems later if ifup and ifdown are used in the "wrong order", where br0 can get IPv6 disabled again or some interfaces (ports) which should have it disabled won't. The only order guaranteed to work is the one from the configuration:

    ifdown br90
    ifdown br5
    ifdown br0.90 # even if they have now disappeared from the system
    ifdown br0.5  # they are still up for ifupdown's logic
    ifdown br0
    ifup br0
    ifup br0.5
    ifup br0.90
    ifup br5
    ifup br90
    
  • keep the bridge being a bridge only and use an additional pair of veth interfaces, with one end on the bridge and one end to participate in routing. This gives a clean separation between bridging and routing (and won't be subject to any side effects, for example when using Docker, but at the same time might require changes in your current setup with Docker):

    auto routing0
    iface routing0 inet static
        pre-up ip link add name routing0 address 9e:7d:01:6c:32:1b type veth peer name br0routing0 || :
        address 172.16.10.35
        netmask 255.255.254.0
        gateway 172.16.10.1
        dns-nameservers 172.16.10.1
    
    iface routing0 inet6 static
        address 2600:####:####:###0::face/64
        dns-nameservers 2600:####:####:###0::1
        gateway 2600:####:####:####0::1
    
    auto br0
    iface br0 inet manual
    bridge_ports br0routing0 enp175s0f1 enp175s0f0 hostveth0
    bridge_stp off
    bridge_maxwait 5
            pre-up ip link add name hostveth0 type veth peer name dockerveth0 || :
            pre-up ip link set hostveth0 up
            pre-up ip link set dockerveth0 up
    

    I don't know if the hardware address is a new one (assumed in the configuration above) or belongs to enp175s0f1 and is needed for some reason (in this case routing0 must not use it, and to avoid complexity don't use this solution). You'll possibly have to adapt the configuration of any unrelated service having br0 in its configuration and use routing0 instead.

  • switch to ifupdown2 which is an ifupdown complete re-implementation made by Cumulus Networks which provides switches and routers running Linux:

    ifupdown2 is a new implementation of debian’s network interface manager ifupdown. It understands interface dependency relationships, simplifies interface configuration, extends ifquery to support interface config validation, supports JSON and more.

    It has built-in bridge and VLAN handling and doesn't rely on the bridge-utils or vlan packages anymore.

    As usual, switching tools managing network might cause connectivity issues, so have a remote console access.

    Keeping your configuration as-is should work correctly, but from this comment in ifupdown2's version of interfaces(5):

    BUILTIN INTERFACES

    iface sections for some interfaces like physical interfaces or vlan interfaces in dot notation (like eth1.100) are understood by ifupdown. These interfaces do not need an entry in the interfaces file if they are dependents of other interfaces and don't need any specific configurations like addresses etc.

    you should completely remove the definitions for br0.5 and br0.90 from the configuration (except of course in the bridge_ports entries).

    Such configuration will get again IPv6 disabled only on bridge ports: br0.5, br0.90 as well as enp175s0f1, enp175s0f0, hostveth0. I still expect possible issues when using arbitrary ifdown/ifup commands.

  • suggestion only: ifupdown2 can also be configured to use VLAN aware bridges, turning the setup into one bridge and zero VLAN sub-interfaces.

    This should be the best setup, but not many applications currently support configuring VLAN IDs on a bridge port (eg: using the bridge vlan command). I don't think Docker supports this, so this would not be useful for OP's setup.