Adguardhome appears to have blocked inter-VLAN traffic

I have an openwrt v24.10 setup running on a Ubiquiti ER-X router.

I have a number of VLANs running. Until very recently, one of those VLANs had a raspberry pi running pihole in it. That was my DNS server. To reduce dependence on the pi, I decided I would try to install adguardhome.

I have done that now.

I set AGH to listen on 0.0.0.0 port 53.

dns:
  bind_hosts:
    - 0.0.0.0
  port: 53

I have moved dnsmasq to port 54.

config dnsmasq
	option domainneeded '1'
	option localise_queries '1'
	option rebind_protection '1'
	option rebind_localhost '1'
	option local '/lan/'
	option domain 'lan'
	option expandhosts '1'
	option cachesize '500'
	option authoritative '1'
	option leasefile '/tmp/dhcp.leases'
	option resolvfile '/tmp/resolv.conf.d/resolv.conf.auto'
	option localservice '0'
	option ednspacket_max '1232'
	option port '54'

Clients in each VLAN are given 192.168.x.1 as their DNS server, through option 6 in the DHCP settings.

config dhcp 'PIGEON_VLAN'
	option interface 'PIGEON_VLAN'
	option start '2'
	option limit '211'
	option leasetime '12h'
	option domain 'lan'
	list dhcp_option '6,192.168.76.1'

config dhcp 'IOT_VLAN'
	option interface 'IOT_VLAN'
	option start '2'
	option limit '249'
	option leasetime '12h'
	option domain 'lan'
	list dhcp_option '6,192.168.20.1'

Devices in each VLAN are using AGH for DNS queries and can access the internet fine.

HOWEVER, something seems to have gone wrong with inter-VLAN traffic. Previously devices in the PIGEON VLAN could ping and access devices in the IOT VLAN. Forwarding for this is defined in firewall thus:

config forwarding
 	option src 'PIGEON_ZONE'
 	option dest 'IOT_ZONE'

I also have these traffic rules:

config rule
	option name 'Allow-Ping'
	option family 'ipv4'
	list proto 'icmp'
	option src '*'
	option target 'ACCEPT'
	option dest '*'

config rule
	option name 'Allow-DNS/DHCP-PIGEON'
	option family 'ipv4'
	option src 'PIGEON_ZONE'
	option dest_port '53 67 68'
	option target 'ACCEPT'

config rule
	option name 'Allow-DNS/DHCP-IOT'
	option family 'ipv4'
	option src 'IOT_ZONE'
	option dest_port '53 67 68'
	option target 'ACCEPT'

But since installing AGH, I can't ping anything in IOT from PIGEON, and, for the devices in IOT which offer webpages, I can't view those from PIGEON either.

If I SSH into the router and ping devices in IOT, that works fine.

Is there something about AGH which affects inter-VLAN traffic? This was all working fine before AGH was installed.

Many thanks

It is highly unlikely that AGH has anything to do with your problem.

Please run this command from the cli.

nft insert rule inet fw4 forward ip saddr 192.168.76.0/24 ip daddr 192.168.20.0/24 counter accept

If it fixes the issue, we will need to see the whole firewall configuration or eventually the output of nft list ruleset.

Hi @pavelgl and thanks for your reply.

I entered the command via SSH. It seemed to accept it OK. I wasn't sure how much resetting I'd need to do before it took effect, so I tried immediately to ping a device in IOT from PIGEON, which failed. Therefore I rebooted the router and tried again, which also failed.

I know you said you'd need to see firewall and the output of nft list ruleset if the command worked but I thought I might as well include them anyway.

firewall:

config defaults
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'REJECT'
	option synflood_protect '1'

config zone
	option name 'PLUSNET_ZONE'
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'REJECT'
	list network 'PLUSNET_WAN'
	option masq '1'
	option mtu_fix '1'

config zone
	option name 'WGVPN_IN_ZONE'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'REJECT'
	list network 'WGVPN_IN_VLAN'

config forwarding
	option src 'WGVPN_IN_ZONE'
	option dest 'PLUSNET_ZONE'

config forwarding
	option src 'WGVPN_IN_ZONE'
	option dest 'PIGEON_ZONE'

config forwarding
	option src 'WGVPN_IN_ZONE'
	option dest 'IOT_ZONE'

config forwarding
	option src 'WGVPN_IN_ZONE'
	option dest 'DNS_ZONE'

config zone
	option name 'WGVPN_OUT_ZONE'
	option input 'REJECT'
	option output 'ACCEPT'
	option forward 'REJECT'
	list network 'WGVPN_OUT_VLAN'
	option masq '1'
	option mtu_fix '1'

config zone
	option name 'PIGEON_ZONE'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'PIGEON_VLAN'

config forwarding
	option src 'PIGEON_ZONE'
	option dest 'PLUSNET_ZONE'

config forwarding
	option src 'PIGEON_ZONE'
	option dest 'WGVPN_IN_ZONE'

config forwarding
	option src 'PIGEON_ZONE'
	option dest 'WGVPN_OUT_ZONE'

config forwarding
	option src 'PIGEON_ZONE'
	option dest 'IOT_ZONE'

config forwarding
	option src 'PIGEON_ZONE'
	option dest 'GUEST_ZONE'

config forwarding
	option src 'PIGEON_ZONE'
	option dest 'DNS_ZONE'

config zone
	option name 'IOT_ZONE'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'IOT_VLAN'

config forwarding
	option src 'IOT_ZONE'
	option dest 'PLUSNET_ZONE'

config forwarding
	option src 'IOT_ZONE'
	option dest 'WGVPN_IN_ZONE'

config forwarding
	option src 'IOT_ZONE'
	option dest 'WGVPN_OUT_ZONE'

config zone
	option name 'GUEST_ZONE'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'GUEST_VLAN'

config forwarding
	option src 'GUEST_ZONE'
	option dest 'PLUSNET_ZONE'

config forwarding
	option src 'GUEST_ZONE'
	option dest 'WGVPN_OUT_ZONE'

config zone
	option name 'WORK_ZONE'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'WORK_VLAN'

config forwarding
	option src 'WORK_ZONE'
	option dest 'PLUSNET_ZONE'

config zone
	option name 'DNS_ZONE'
	option input 'ACCEPT'
	option output 'ACCEPT'
	option forward 'ACCEPT'
	list network 'DNS_VLAN'

config forwarding
	option src 'DNS_ZONE'
	option dest 'PLUSNET_ZONE'

config forwarding
	option src 'DNS_ZONE'
	option dest 'WGVPN_IN_ZONE'

config forwarding
	option src 'DNS_ZONE'
	option dest 'WGVPN_OUT_ZONE'

config rule
	option name 'Block-Ping-WORK-to-Other-Zones (keep WORK completely separate)'
	option family 'ipv4'
	option proto 'icmp'
	option src 'WORK_ZONE'
	option dest '*'
	option target 'REJECT'

config rule
	option name 'Block-DNS-Forwarding-WORK-to-DNS (keep WORK completely separate)'
	option family 'ipv4'
	option src 'WORK_ZONE'
	option dest 'DNS_ZONE'
	option dest_port '53'
	list dest_ip '192.168.50.50'
	option target 'REJECT'

config rule
	option name 'Allow-DNS/DHCP-PIGEON'
	option family 'ipv4'
	option src 'PIGEON_ZONE'
	option dest_port '53 67 68'
	option target 'ACCEPT'

config rule
	option name 'Allow-DNS/DHCP-IOT'
	option family 'ipv4'
	option src 'IOT_ZONE'
	option dest_port '53 67 68'
	option target 'ACCEPT'

config rule
	option name 'Allow-DNS/DHCP-GUEST'
	option family 'ipv4'
	option src 'GUEST_ZONE'
	option dest_port '53 67 68'
	option target 'ACCEPT'

config rule
	option name 'Allow-DNS/DHCP-WORK'
	option family 'ipv4'
	option src 'WORK_ZONE'
	option dest_port '53 67 68'
	option target 'ACCEPT'

config rule
	option name 'Allow-DNS/DHCP-DNS'
	option family 'ipv4'
	option src 'DNS_ZONE'
	option dest_port '53 67 68'
	option target 'ACCEPT'

config rule
	option name 'Allow-Ping'
	option family 'ipv4'
	list proto 'icmp'
	option src '*'
	option target 'ACCEPT'
	option dest '*'

config rule
	option name 'Allow-DHCP-Renew'
	option family 'ipv4'
	list proto 'udp'
	option src '*'
	option dest_port '68'
	option target 'ACCEPT'

config rule
	option name 'Allow mDNS (for casting)'
	option family 'ipv4'
	list proto 'udp'
	option src '*'
	option dest '*'
	list dest_ip '224.0.0.251'
	option dest_port '5353'
	option target 'ACCEPT'

config rule
	option name 'Allow Marantz to be casted to by specified devices'
	option family 'ipv4'
	list proto 'all'
	option src 'IOT_ZONE'
	list src_ip '192.168.20.7'
	option dest 'PIGEON_ZONE'
	list dest_ip '192.168.76.3'
	list dest_ip '192.168.76.6'
	list dest_ip '192.168.76.14'
	list dest_ip '192.168.76.15'
	option target 'ACCEPT'

config rule
	option name 'Allow PVR to see SynologyNAS1'
	option family 'ipv4'
	list proto 'tcp'
	list proto 'udp'
	list proto 'icmp'
	option src 'IOT_ZONE'
	list src_ip '192.168.20.50'
	option dest 'PIGEON_ZONE'
	list dest_ip '192.168.76.202'
	option target 'ACCEPT'

config rule
	option name 'Allow Marantz to see SynologyNAS1'
	option family 'ipv4'
	list proto 'all'
	option src 'IOT_ZONE'
	list src_ip '192.168.20.7'
	option dest 'PIGEON_ZONE'
	list dest_ip '192.168.76.202'
	option target 'ACCEPT'

config rule
	option name 'Allow AppleTV to see SynologyNAS1'
	option family 'ipv4'
	list proto 'all'
	option src 'IOT_ZONE'
	list src_ip '192.168.20.3'
	list src_ip '192.168.20.4'
	option dest 'PIGEON_ZONE'
	list dest_ip '192.168.76.202'
	option target 'ACCEPT'

config rule
	option name 'Allow-Ping-Forwarding-PIGEON-To-AnyZone'
	option family 'ipv4'
	list proto 'icmp'
	option src 'PIGEON_ZONE'
	option dest '*'
	option target 'ACCEPT'

config rule
	option name 'Allow-Ping-Forwarding-IOT-To-DNS'
	option family 'ipv4'
	list proto 'icmp'
	option src 'IOT_ZONE'
	option dest 'DNS_ZONE'
	option target 'ACCEPT'

config rule
	option name 'Allow-Ping-Forwarding-GUEST-To-DNS'
	option family 'ipv4'
	list proto 'icmp'
	option src 'GUEST_ZONE'
	option dest 'DNS_ZONE'
	option target 'ACCEPT'

config redirect
	option name 'NASVPN'
	option src 'PLUSNET_ZONE'
	option src_dport '1195'
	option dest 'PIGEON_ZONE'
	option dest_ip '192.168.76.202'
	option dest_port '1195'
	option target 'DNAT'

config redirect
	option name 'WGVPN_IN'
	option src 'PLUSNET_ZONE'
	option src_dport '51820'
	option dest 'WGVPN_IN_ZONE'
	option dest_ip '10.76.0.1'
	option dest_port '51820'
	option target 'DNAT'

config include 'pbr'
	option fw4_compatible '1'
	option type 'script'
	option path '/usr/share/pbr/firewall.include'

(DNS ZONE is still being created but I'm not knowingly using the pihole sitting in it for anything)

I'll put the command output in the next post as it's too long for this one.

nft list ruleset output (if it matters, this is AFTER the reboot mentioned above:

root@router:~# nft list ruleset
table inet fw4 {
        chain input {
                type filter hook input priority filter; policy drop;
                iif "lo" accept comment "!fw4: Accept traffic from loopback"
                ct state vmap { established : accept, related : accept } comment "!fw4: Handle inbound flows"
                tcp flags & (fin | syn | rst | ack) == syn jump syn_flood comment "!fw4: Rate limit TCP syn packets"
                meta nfproto ipv4 udp dport 68 counter packets 0 bytes 0 accept comment "!fw4: Allow-DHCP-Renew"
                iifname "pppoe-PLUSNET_W" jump input_PLUSNET_ZONE comment "!fw4: Handle PLUSNET_ZONE IPv4/IPv6 input traffic"
                iifname "WGVPN_IN_VLAN" jump input_WGVPN_IN_ZONE comment "!fw4: Handle WGVPN_IN_ZONE IPv4/IPv6 input traffic"
                iifname "WGVPN_OUT_VLAN" jump input_WGVPN_OUT_ZONE comment "!fw4: Handle WGVPN_OUT_ZONE IPv4/IPv6 input traffic"
                iifname "br-lan.1" jump input_PIGEON_ZONE comment "!fw4: Handle PIGEON_ZONE IPv4/IPv6 input traffic"
                iifname "br-lan.20" jump input_IOT_ZONE comment "!fw4: Handle IOT_ZONE IPv4/IPv6 input traffic"
                iifname "br-lan.30" jump input_GUEST_ZONE comment "!fw4: Handle GUEST_ZONE IPv4/IPv6 input traffic"
                iifname "br-lan.40" jump input_WORK_ZONE comment "!fw4: Handle WORK_ZONE IPv4/IPv6 input traffic"
                iifname "br-lan.50" jump input_DNS_ZONE comment "!fw4: Handle DNS_ZONE IPv4/IPv6 input traffic"
                jump handle_reject
        }

        chain forward {
                type filter hook forward priority filter; policy drop;
                ct state vmap { established : accept, related : accept } comment "!fw4: Handle forwarded flows"
                meta l4proto icmp counter packets 4 bytes 264 accept comment "!fw4: Allow-Ping"
                ip daddr 224.0.0.251 udp dport 5353 counter packets 0 bytes 0 accept comment "!fw4: Allow mDNS (for casting.)"
                iifname "pppoe-PLUSNET_W" jump forward_PLUSNET_ZONE comment "!fw4: Handle PLUSNET_ZONE IPv4/IPv6 forward traffic"
                iifname "WGVPN_IN_VLAN" jump forward_WGVPN_IN_ZONE comment "!fw4: Handle WGVPN_IN_ZONE IPv4/IPv6 forward traffic"
                iifname "WGVPN_OUT_VLAN" jump forward_WGVPN_OUT_ZONE comment "!fw4: Handle WGVPN_OUT_ZONE IPv4/IPv6 forward traffic"
                iifname "br-lan.1" jump forward_PIGEON_ZONE comment "!fw4: Handle PIGEON_ZONE IPv4/IPv6 forward traffic"
                iifname "br-lan.20" jump forward_IOT_ZONE comment "!fw4: Handle IOT_ZONE IPv4/IPv6 forward traffic"
                iifname "br-lan.30" jump forward_GUEST_ZONE comment "!fw4: Handle GUEST_ZONE IPv4/IPv6 forward traffic"
                iifname "br-lan.40" jump forward_WORK_ZONE comment "!fw4: Handle WORK_ZONE IPv4/IPv6 forward traffic"
                iifname "br-lan.50" jump forward_DNS_ZONE comment "!fw4: Handle DNS_ZONE IPv4/IPv6 forward traffic"
                jump handle_reject
        }

        chain output {
                type filter hook output priority filter; policy accept;
                oif "lo" accept comment "!fw4: Accept traffic towards loopback"
                ct state vmap { established : accept, related : accept } comment "!fw4: Handle outbound flows"
                oifname "pppoe-PLUSNET_W" jump output_PLUSNET_ZONE comment "!fw4: Handle PLUSNET_ZONE IPv4/IPv6 output traffic"
                oifname "WGVPN_IN_VLAN" jump output_WGVPN_IN_ZONE comment "!fw4: Handle WGVPN_IN_ZONE IPv4/IPv6 output traffic"
                oifname "WGVPN_OUT_VLAN" jump output_WGVPN_OUT_ZONE comment "!fw4: Handle WGVPN_OUT_ZONE IPv4/IPv6 output traffic"
                oifname "br-lan.1" jump output_PIGEON_ZONE comment "!fw4: Handle PIGEON_ZONE IPv4/IPv6 output traffic"
                oifname "br-lan.20" jump output_IOT_ZONE comment "!fw4: Handle IOT_ZONE IPv4/IPv6 output traffic"
                oifname "br-lan.30" jump output_GUEST_ZONE comment "!fw4: Handle GUEST_ZONE IPv4/IPv6 output traffic"
                oifname "br-lan.40" jump output_WORK_ZONE comment "!fw4: Handle WORK_ZONE IPv4/IPv6 output traffic"
                oifname "br-lan.50" jump output_DNS_ZONE comment "!fw4: Handle DNS_ZONE IPv4/IPv6 output traffic"
        }

        chain prerouting {
                type filter hook prerouting priority filter; policy accept;
                iifname "WGVPN_IN_VLAN" jump helper_WGVPN_IN_ZONE comment "!fw4: Handle WGVPN_IN_ZONE IPv4/IPv6 helper assignment"
                iifname "br-lan.1" jump helper_PIGEON_ZONE comment "!fw4: Handle PIGEON_ZONE IPv4/IPv6 helper assignment"
                iifname "br-lan.20" jump helper_IOT_ZONE comment "!fw4: Handle IOT_ZONE IPv4/IPv6 helper assignment"
                iifname "br-lan.30" jump helper_GUEST_ZONE comment "!fw4: Handle GUEST_ZONE IPv4/IPv6 helper assignment"
                iifname "br-lan.40" jump helper_WORK_ZONE comment "!fw4: Handle WORK_ZONE IPv4/IPv6 helper assignment"
                iifname "br-lan.50" jump helper_DNS_ZONE comment "!fw4: Handle DNS_ZONE IPv4/IPv6 helper assignment"
        }

        chain handle_reject {
                meta l4proto tcp reject with tcp reset comment "!fw4: Reject TCP traffic"
                reject comment "!fw4: Reject any other traffic"
        }

        chain syn_flood {
                limit rate 25/second burst 50 packets return comment "!fw4: Accept SYN packets below rate-limit"
                drop comment "!fw4: Drop excess packets"
        }

        chain input_PLUSNET_ZONE {
                ct status dnat accept comment "!fw4: Accept port redirections"
                jump reject_from_PLUSNET_ZONE
        }

        chain output_PLUSNET_ZONE {
                jump accept_to_PLUSNET_ZONE
        }

        chain forward_PLUSNET_ZONE {
                ct status dnat accept comment "!fw4: Accept port forwards"
                jump reject_to_PLUSNET_ZONE
        }

        chain accept_to_PLUSNET_ZONE {
                meta nfproto ipv4 oifname "pppoe-PLUSNET_W" ct state invalid counter packets 8 bytes 320 drop comment "!fw4: Prevent NAT leakage"
                oifname "pppoe-PLUSNET_W" counter packets 1844 bytes 202258 accept comment "!fw4: accept PLUSNET_ZONE IPv4/IPv6 traffic"
        }

        chain reject_from_PLUSNET_ZONE {
                iifname "pppoe-PLUSNET_W" counter packets 1793 bytes 193746 jump handle_reject comment "!fw4: reject PLUSNET_ZONE IPv4/IPv6 traffic"
        }

        chain reject_to_PLUSNET_ZONE {
                oifname "pppoe-PLUSNET_W" counter packets 0 bytes 0 jump handle_reject comment "!fw4: reject PLUSNET_ZONE IPv4/IPv6 traffic"
        }

        chain input_WGVPN_IN_ZONE {
                ct status dnat accept comment "!fw4: Accept port redirections"
                jump accept_from_WGVPN_IN_ZONE
        }

        chain output_WGVPN_IN_ZONE {
                jump accept_to_WGVPN_IN_ZONE
        }

        chain forward_WGVPN_IN_ZONE {
                jump accept_to_PLUSNET_ZONE comment "!fw4: Accept WGVPN_IN_ZONE to PLUSNET_ZONE forwarding"
                jump accept_to_PIGEON_ZONE comment "!fw4: Accept WGVPN_IN_ZONE to PIGEON_ZONE forwarding"
                jump accept_to_IOT_ZONE comment "!fw4: Accept WGVPN_IN_ZONE to IOT_ZONE forwarding"
                jump accept_to_DNS_ZONE comment "!fw4: Accept WGVPN_IN_ZONE to DNS_ZONE forwarding"
                ct status dnat accept comment "!fw4: Accept port forwards"
                jump reject_to_WGVPN_IN_ZONE
        }

        chain helper_WGVPN_IN_ZONE {
        }

        chain accept_from_WGVPN_IN_ZONE {
                iifname "WGVPN_IN_VLAN" counter packets 0 bytes 0 accept comment "!fw4: accept WGVPN_IN_ZONE IPv4/IPv6 traffic"
        }

        chain accept_to_WGVPN_IN_ZONE {
                oifname "WGVPN_IN_VLAN" counter packets 0 bytes 0 accept comment "!fw4: accept WGVPN_IN_ZONE IPv4/IPv6 traffic"
        }

        chain reject_to_WGVPN_IN_ZONE {
                oifname "WGVPN_IN_VLAN" counter packets 0 bytes 0 jump handle_reject comment "!fw4: reject WGVPN_IN_ZONE IPv4/IPv6 traffic"
        }

        chain input_WGVPN_OUT_ZONE {
                jump reject_from_WGVPN_OUT_ZONE
        }

        chain output_WGVPN_OUT_ZONE {
                jump accept_to_WGVPN_OUT_ZONE
        }

        chain forward_WGVPN_OUT_ZONE {
                jump reject_to_WGVPN_OUT_ZONE
        }

        chain accept_to_WGVPN_OUT_ZONE {
                meta nfproto ipv4 oifname "WGVPN_OUT_VLAN" ct state invalid counter packets 57 bytes 2532 drop comment "!fw4: Prevent NAT leakage"
                oifname "WGVPN_OUT_VLAN" counter packets 1024 bytes 152178 accept comment "!fw4: accept WGVPN_OUT_ZONE IPv4/IPv6 traffic"
        }

        chain reject_from_WGVPN_OUT_ZONE {
                iifname "WGVPN_OUT_VLAN" counter packets 55 bytes 15801 jump handle_reject comment "!fw4: reject WGVPN_OUT_ZONE IPv4/IPv6 traffic"
        }

        chain reject_to_WGVPN_OUT_ZONE {
                oifname "WGVPN_OUT_VLAN" counter packets 0 bytes 0 jump handle_reject comment "!fw4: reject WGVPN_OUT_ZONE IPv4/IPv6 traffic"
        }

        chain input_PIGEON_ZONE {
                meta nfproto ipv4 meta l4proto igmp counter packets 213 bytes 6848 accept comment "!fw4: ubus:igmpproxy[instance1] rule 5"
                meta nfproto ipv4 tcp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-PIGEON"
                meta nfproto ipv4 udp dport { 53, 67, 68 } counter packets 387 bytes 28723 accept comment "!fw4: Allow-DNS/DHCP-PIGEON"
                ct status dnat accept comment "!fw4: Accept port redirections"
                jump accept_from_PIGEON_ZONE
        }

        chain output_PIGEON_ZONE {
                jump accept_to_PIGEON_ZONE
        }

        chain forward_PIGEON_ZONE {
                meta l4proto icmp counter packets 0 bytes 0 accept comment "!fw4: Allow-Ping-Forwarding-PIGEON-To-AnyZone"
                jump accept_to_PLUSNET_ZONE comment "!fw4: Accept PIGEON_ZONE to PLUSNET_ZONE forwarding"
                jump accept_to_WGVPN_IN_ZONE comment "!fw4: Accept PIGEON_ZONE to WGVPN_IN_ZONE forwarding"
                jump accept_to_WGVPN_OUT_ZONE comment "!fw4: Accept PIGEON_ZONE to WGVPN_OUT_ZONE forwarding"
                jump accept_to_IOT_ZONE comment "!fw4: Accept PIGEON_ZONE to IOT_ZONE forwarding"
                jump accept_to_GUEST_ZONE comment "!fw4: Accept PIGEON_ZONE to GUEST_ZONE forwarding"
                jump accept_to_DNS_ZONE comment "!fw4: Accept PIGEON_ZONE to DNS_ZONE forwarding"
                ct status dnat accept comment "!fw4: Accept port forwards"
                jump accept_to_PIGEON_ZONE
        }

        chain helper_PIGEON_ZONE {
        }

        chain accept_from_PIGEON_ZONE {
                iifname "br-lan.1" counter packets 2652 bytes 278547 accept comment "!fw4: accept PIGEON_ZONE IPv4/IPv6 traffic"
        }

        chain accept_to_PIGEON_ZONE {
                oifname "br-lan.1" counter packets 994 bytes 108878 accept comment "!fw4: accept PIGEON_ZONE IPv4/IPv6 traffic"
        }

        chain input_IOT_ZONE {
                meta nfproto ipv4 meta l4proto igmp counter packets 19 bytes 608 accept comment "!fw4: ubus:igmpproxy[instance1] rule 0"
                meta nfproto ipv4 tcp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-IOT"
                meta nfproto ipv4 udp dport { 53, 67, 68 } counter packets 99 bytes 18722 accept comment "!fw4: Allow-DNS/DHCP-IOT"
                jump accept_from_IOT_ZONE
        }

        chain output_IOT_ZONE {
                jump accept_to_IOT_ZONE
        }

        chain forward_IOT_ZONE {
                meta l4proto udp ip daddr 239.255.255.250 counter packets 17 bytes 6175 jump drop_to_PIGEON_ZONE comment "!fw4: ubus:igmpproxy[instance1] rule 1"
                meta l4proto udp ip daddr 224.0.0.0/4 counter packets 2 bytes 702 jump accept_to_PIGEON_ZONE comment "!fw4: ubus:igmpproxy[instance1] rule 2"
                meta l4proto udp ip daddr 239.255.255.250 counter packets 0 bytes 0 jump drop_to_DNS_ZONE comment "!fw4: ubus:igmpproxy[instance1] rule 3"
                meta l4proto udp ip daddr 224.0.0.0/4 counter packets 2 bytes 702 jump accept_to_DNS_ZONE comment "!fw4: ubus:igmpproxy[instance1] rule 4"
                ip saddr 192.168.20.7 ip daddr { 192.168.76.3, 192.168.76.6, 192.168.76.14, 192.168.76.15 } counter packets 0 bytes 0 jump accept_to_PIGEON_ZONE comment "!fw4: Allow Marantz to be casted to by specified devices"
                meta l4proto tcp ip saddr 192.168.20.50 ip daddr 192.168.76.202 counter packets 0 bytes 0 jump accept_to_PIGEON_ZONE comment "!fw4: Allow PVR to see SynologyNAS1"
                meta l4proto udp ip saddr 192.168.20.50 ip daddr 192.168.76.202 counter packets 0 bytes 0 jump accept_to_PIGEON_ZONE comment "!fw4: Allow PVR to see SynologyNAS1"
                meta l4proto icmp ip saddr 192.168.20.50 ip daddr 192.168.76.202 counter packets 0 bytes 0 jump accept_to_PIGEON_ZONE comment "!fw4: Allow PVR to see SynologyNAS1"
                ip saddr 192.168.20.7 ip daddr 192.168.76.202 counter packets 0 bytes 0 jump accept_to_PIGEON_ZONE comment "!fw4: Allow Marantz to see SynologyNAS1"
                ip saddr { 192.168.20.3, 192.168.20.4 } ip daddr 192.168.76.202 counter packets 0 bytes 0 jump accept_to_PIGEON_ZONE comment "!fw4: Allow AppleTV to see SynologyNAS1"
                meta l4proto icmp counter packets 0 bytes 0 jump accept_to_DNS_ZONE comment "!fw4: Allow-Ping-Forwarding-IOT-To-DNS"
                jump accept_to_PLUSNET_ZONE comment "!fw4: Accept IOT_ZONE to PLUSNET_ZONE forwarding"
                jump accept_to_WGVPN_IN_ZONE comment "!fw4: Accept IOT_ZONE to WGVPN_IN_ZONE forwarding"
                jump accept_to_WGVPN_OUT_ZONE comment "!fw4: Accept IOT_ZONE to WGVPN_OUT_ZONE forwarding"
                jump accept_to_IOT_ZONE
        }

        chain helper_IOT_ZONE {
        }

        chain accept_from_IOT_ZONE {
                iifname "br-lan.20" counter packets 1632 bytes 208803 accept comment "!fw4: accept IOT_ZONE IPv4/IPv6 traffic"
        }

        chain accept_to_IOT_ZONE {
                oifname "br-lan.20" counter packets 814 bytes 97403 accept comment "!fw4: accept IOT_ZONE IPv4/IPv6 traffic"
        }

        chain input_GUEST_ZONE {
                meta nfproto ipv4 tcp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-GUEST"
                meta nfproto ipv4 udp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-GUEST"
                jump accept_from_GUEST_ZONE
        }

        chain output_GUEST_ZONE {
                jump accept_to_GUEST_ZONE
        }

        chain forward_GUEST_ZONE {
                meta l4proto icmp counter packets 0 bytes 0 jump accept_to_DNS_ZONE comment "!fw4: Allow-Ping-Forwarding-GUEST-To-DNS"
                jump accept_to_PLUSNET_ZONE comment "!fw4: Accept GUEST_ZONE to PLUSNET_ZONE forwarding"
                jump accept_to_WGVPN_OUT_ZONE comment "!fw4: Accept GUEST_ZONE to WGVPN_OUT_ZONE forwarding"
                jump accept_to_GUEST_ZONE
        }

        chain helper_GUEST_ZONE {
        }

        chain accept_from_GUEST_ZONE {
                iifname "br-lan.30" counter packets 1576 bytes 180360 accept comment "!fw4: accept GUEST_ZONE IPv4/IPv6 traffic"
        }

        chain accept_to_GUEST_ZONE {
                oifname "br-lan.30" counter packets 1576 bytes 180360 accept comment "!fw4: accept GUEST_ZONE IPv4/IPv6 traffic"
        }

        chain input_WORK_ZONE {
                meta nfproto ipv4 tcp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-WORK"
                meta nfproto ipv4 udp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-WORK"
                jump accept_from_WORK_ZONE
        }

        chain output_WORK_ZONE {
                jump accept_to_WORK_ZONE
        }

        chain forward_WORK_ZONE {
                meta l4proto icmp counter packets 0 bytes 0 jump handle_reject comment "!fw4: Block-Ping-WORK-to-Other-Zones (keep WORK completely separate)"
                ip daddr 192.168.50.50 tcp dport 53 counter packets 0 bytes 0 jump reject_to_DNS_ZONE comment "!fw4: Block-DNS-Forwarding-WORK-to-DNS (keep WORK completely separate)"
                ip daddr 192.168.50.50 udp dport 53 counter packets 0 bytes 0 jump reject_to_DNS_ZONE comment "!fw4: Block-DNS-Forwarding-WORK-to-DNS (keep WORK completely separate)"
                jump accept_to_PLUSNET_ZONE comment "!fw4: Accept WORK_ZONE to PLUSNET_ZONE forwarding"
                jump accept_to_WORK_ZONE
        }

        chain helper_WORK_ZONE {
        }

        chain accept_from_WORK_ZONE {
                iifname "br-lan.40" counter packets 1576 bytes 180360 accept comment "!fw4: accept WORK_ZONE IPv4/IPv6 traffic"
        }

        chain accept_to_WORK_ZONE {
                oifname "br-lan.40" counter packets 1576 bytes 180360 accept comment "!fw4: accept WORK_ZONE IPv4/IPv6 traffic"
        }

        chain input_DNS_ZONE {
                meta nfproto ipv4 meta l4proto igmp counter packets 210 bytes 6720 accept comment "!fw4: ubus:igmpproxy[instance1] rule 6"
                meta nfproto ipv4 tcp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-DNS"
                meta nfproto ipv4 udp dport { 53, 67, 68 } counter packets 0 bytes 0 accept comment "!fw4: Allow-DNS/DHCP-DNS"
                jump accept_from_DNS_ZONE
        }

        chain output_DNS_ZONE {
                jump accept_to_DNS_ZONE
        }

        chain forward_DNS_ZONE {
                jump accept_to_PLUSNET_ZONE comment "!fw4: Accept DNS_ZONE to PLUSNET_ZONE forwarding"
                jump accept_to_WGVPN_IN_ZONE comment "!fw4: Accept DNS_ZONE to WGVPN_IN_ZONE forwarding"
                jump accept_to_WGVPN_OUT_ZONE comment "!fw4: Accept DNS_ZONE to WGVPN_OUT_ZONE forwarding"
                jump accept_to_DNS_ZONE
        }

        chain helper_DNS_ZONE {
        }

        chain accept_from_DNS_ZONE {
                iifname "br-lan.50" counter packets 1576 bytes 180360 accept comment "!fw4: accept DNS_ZONE IPv4/IPv6 traffic"
        }

        chain accept_to_DNS_ZONE {
                oifname "br-lan.50" counter packets 1743 bytes 186342 accept comment "!fw4: accept DNS_ZONE IPv4/IPv6 traffic"
        }

        chain reject_to_DNS_ZONE {
                oifname "br-lan.50" counter packets 0 bytes 0 jump handle_reject comment "!fw4: reject DNS_ZONE IPv4/IPv6 traffic"
        }

        chain dstnat {
                type nat hook prerouting priority dstnat; policy accept;
                jump pbr_dstnat comment "Jump into pbr dstnat chain"
                iifname "pppoe-PLUSNET_W" jump dstnat_PLUSNET_ZONE comment "!fw4: Handle PLUSNET_ZONE IPv4/IPv6 dstnat traffic"
                iifname "WGVPN_IN_VLAN" jump dstnat_WGVPN_IN_ZONE comment "!fw4: Handle WGVPN_IN_ZONE IPv4/IPv6 dstnat traffic"
                iifname "br-lan.1" jump dstnat_PIGEON_ZONE comment "!fw4: Handle PIGEON_ZONE IPv4/IPv6 dstnat traffic"
        }

        chain srcnat {
                type nat hook postrouting priority srcnat; policy accept;
                oifname "pppoe-PLUSNET_W" jump srcnat_PLUSNET_ZONE comment "!fw4: Handle PLUSNET_ZONE IPv4/IPv6 srcnat traffic"
                oifname "WGVPN_IN_VLAN" jump srcnat_WGVPN_IN_ZONE comment "!fw4: Handle WGVPN_IN_ZONE IPv4/IPv6 srcnat traffic"
                oifname "WGVPN_OUT_VLAN" jump srcnat_WGVPN_OUT_ZONE comment "!fw4: Handle WGVPN_OUT_ZONE IPv4/IPv6 srcnat traffic"
                oifname "br-lan.1" jump srcnat_PIGEON_ZONE comment "!fw4: Handle PIGEON_ZONE IPv4/IPv6 srcnat traffic"
        }

        chain dstnat_PLUSNET_ZONE {
                meta nfproto ipv4 tcp dport 1195 counter packets 0 bytes 0 dnat ip to 192.168.76.202:1195 comment "!fw4: NASVPN"
                meta nfproto ipv4 udp dport 1195 counter packets 0 bytes 0 dnat ip to 192.168.76.202:1195 comment "!fw4: NASVPN"
                meta nfproto ipv4 tcp dport 51820 counter packets 0 bytes 0 dnat ip to 10.76.0.1:51820 comment "!fw4: WGVPN_IN"
                meta nfproto ipv4 udp dport 51820 counter packets 0 bytes 0 dnat ip to 10.76.0.1:51820 comment "!fw4: WGVPN_IN"
        }

        chain srcnat_PLUSNET_ZONE {
                meta nfproto ipv4 masquerade comment "!fw4: Masquerade IPv4 PLUSNET_ZONE traffic"
        }

        chain dstnat_WGVPN_IN_ZONE {
                ip saddr 10.76.0.0/24 ip daddr x.x.x.x tcp dport 51820 dnat ip to 10.76.0.1:51820 comment "!fw4: WGVPN_IN (reflection)"
                ip saddr 10.76.0.0/24 ip daddr x.x.x.x udp dport 51820 dnat ip to 10.76.0.1:51820 comment "!fw4: WGVPN_IN (reflection)"
        }

        chain srcnat_WGVPN_IN_ZONE {
                ip saddr 10.76.0.0/24 ip daddr 10.76.0.1 tcp dport 51820 snat ip to 10.76.0.1 comment "!fw4: WGVPN_IN (reflection)"
                ip saddr 10.76.0.0/24 ip daddr 10.76.0.1 udp dport 51820 snat ip to 10.76.0.1 comment "!fw4: WGVPN_IN (reflection)"
        }

        chain srcnat_WGVPN_OUT_ZONE {
                meta nfproto ipv4 masquerade comment "!fw4: Masquerade IPv4 WGVPN_OUT_ZONE traffic"
        }

        chain dstnat_PIGEON_ZONE {
                ip saddr 192.168.76.0/24 ip daddr x.x.x.x tcp dport 1195 dnat ip to 192.168.76.202:1195 comment "!fw4: NASVPN (reflection)"
                ip saddr 192.168.76.0/24 ip daddr x.x.x.x udp dport 1195 dnat ip to 192.168.76.202:1195 comment "!fw4: NASVPN (reflection)"
        }

        chain srcnat_PIGEON_ZONE {
                ip saddr 192.168.76.0/24 ip daddr 192.168.76.202 tcp dport 1195 snat ip to 192.168.76.1 comment "!fw4: NASVPN (reflection)"
                ip saddr 192.168.76.0/24 ip daddr 192.168.76.202 udp dport 1195 snat ip to 192.168.76.1 comment "!fw4: NASVPN (reflection)"
        }

        chain raw_prerouting {
                type filter hook prerouting priority raw; policy accept;
        }

        chain raw_output {
                type filter hook output priority raw; policy accept;
        }

        chain mangle_prerouting {
                type filter hook prerouting priority mangle; policy accept;
                jump pbr_prerouting comment "Jump into pbr prerouting chain"
        }

        chain mangle_postrouting {
                type filter hook postrouting priority mangle; policy accept;
                oifname "pppoe-PLUSNET_W" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone PLUSNET_ZONE IPv4/IPv6 egress MTU fixing"
                oifname "WGVPN_OUT_VLAN" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone WGVPN_OUT_ZONE IPv4/IPv6 egress MTU fixing"
                jump pbr_postrouting comment "Jump into pbr postrouting chain"
        }

        chain mangle_input {
                type filter hook input priority mangle; policy accept;
                jump pbr_input comment "Jump into pbr input chain"
        }

        chain mangle_output {
                type route hook output priority mangle; policy accept;
                jump pbr_output comment "Jump into pbr output chain"
        }

        chain mangle_forward {
                type filter hook forward priority mangle; policy accept;
                iifname "pppoe-PLUSNET_W" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone PLUSNET_ZONE IPv4/IPv6 ingress MTU fixing"
                iifname "WGVPN_OUT_VLAN" tcp flags & (fin | syn | rst) == syn tcp option maxseg size set rt mtu comment "!fw4: Zone WGVPN_OUT_ZONE IPv4/IPv6 ingress MTU fixing"
                jump pbr_forward comment "Jump into pbr forward chain"
        }

        chain pbr_dstnat {
        }

        chain pbr_forward {
        }

        chain pbr_input {
        }

        chain pbr_output {
        }

        chain pbr_prerouting {
                ip saddr { 192.168.20.0/24, 192.168.30.0/24, 192.168.76.0/24 } ip daddr 192.168.50.50 tcp sport 53 tcp dport 53 return comment "DNS queries (make sure all port 53 goes to pihole)"
                ip saddr { 192.168.20.0/24, 192.168.30.0/24, 192.168.76.0/24 } ip daddr 192.168.50.50 udp sport 53 udp dport 53 return comment "DNS queries (make sure all port 53 goes to pihole)"
                ip saddr 192.168.76.0/24 ip daddr { 132.185.0.0/16, 151.101.0.0/16, 185.184.237.181, 212.58.224.0/19 } goto pbr_mark_0x010000 comment "BBC via PLUSNET (Keep BBC website/iPlayer traffic out of the outgoing VPN)"
                ip saddr 192.168.76.0/24 ip daddr { 2.18.190.0/24, 2.19.117.0/24, 2.22.144.0/24, 13.224.0.0/14, 18.165.0.0/16, 104.86.110.0/23, 151.101.0.0/16, 184.50.112.0/23, 212.58.224.0/19 } goto pbr_mark_0x010000 comment "BBC Podcasts via PLUSNET (Keep bbc podcast traffic out of the outgoing VPN)"
                ip saddr 192.168.76.202 tcp sport 1195 goto pbr_mark_0x010000 comment "NASVPN outwards"
                ip saddr 192.168.76.202 udp sport 1195 goto pbr_mark_0x010000 comment "NASVPN outwards"
                ip saddr 192.168.76.0/24 goto pbr_mark_0x020000 comment "PIGEON to WAN (decide how the rest of PIGEON packets are connected to the internet) "
                ip saddr 192.168.20.0/24 goto pbr_mark_0x020000 comment "IOT to WAN (decide how the rest of IOT packets are connected to the internet)"
                ip saddr 192.168.30.0/24 goto pbr_mark_0x020000 comment "GUEST to WAN (decide how the rest of GUEST packets are connected to the internet)"
                ip saddr 192.168.40.0/24 goto pbr_mark_0x010000 comment "WORK to WAN (decide how the rest of WORK packets are connected to the internet)"
                ip saddr 192.168.50.0/24 goto pbr_mark_0x010000 comment "DNS to WAN (decide how the rest of DNS packets are connected to the internet)"
                ip saddr 10.76.0.0/24 goto pbr_mark_0x010000 comment "WGVPN_IN (decide how incoming VPN is connected to the internet)"
        }

        chain pbr_postrouting {
        }

        chain drop_to_PIGEON_ZONE {
                oifname "br-lan.1" counter packets 17 bytes 6175 drop comment "!fw4: drop PIGEON_ZONE IPv4/IPv6 traffic"
        }

        chain drop_to_DNS_ZONE {
                oifname "br-lan.50" counter packets 0 bytes 0 drop comment "!fw4: drop DNS_ZONE IPv4/IPv6 traffic"
        }

        chain pbr_mark_0x010000 {
                meta mark set meta mark & 0xff01ffff | 0x00010000
                return
        }

        chain pbr_mark_0x020000 {
                meta mark set meta mark & 0xff02ffff | 0x00020000
                return
        }
}

Thank you if you can spot anything wrong.

Well, the firewall can be optimized, but it doesn't seem to be the cause of the problem.

It appears you are also using pbr.

Let's see if we can find something wrong with the routing.

ip -4 addr; echo ""; ip ru; echo ""; ip -4 ro li ta all

You know what to redact.

Here is the output of the command:

root@router:~# ip -4 addr; echo ""; ip ru; echo ""; ip -4 ro li ta al
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
9: br-lan.1@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.76.1/24 brd 192.168.76.255 scope global br-lan.1
       valid_lft forever preferred_lft forever
10: br-lan.20@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.20.1/24 brd 192.168.20.255 scope global br-lan.20
       valid_lft forever preferred_lft forever
11: br-lan.30@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.30.1/24 brd 192.168.30.255 scope global br-lan.30
       valid_lft forever preferred_lft forever
12: br-lan.40@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.40.1/24 brd 192.168.40.255 scope global br-lan.40
       valid_lft forever preferred_lft forever
13: br-lan.50@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.50.1/24 brd 192.168.50.255 scope global br-lan.50
       valid_lft forever preferred_lft forever
14: WGVPN_IN_VLAN: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.76.0.1/24 brd 10.76.0.255 scope global WGVPN_IN_VLAN
       valid_lft forever preferred_lft forever
15: WGVPN_OUT_VLAN: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.14.0.2/16 brd 10.14.255.255 scope global WGVPN_OUT_VLAN
       valid_lft forever preferred_lft forever
16: pppoe-PLUSNET_W: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UNKNOWN group default qlen 3
    inet x.x.x.x peer 195.166.x.x/32 scope global pppoe-PLUSNET_W
       valid_lft forever preferred_lft forever

0:      from all lookup local
29997:  from all sport 51820 lookup pbr_PLUSNET_WAN
29998:  from all fwmark 0x20000/0xff0000 lookup pbr_WGVPN_OUT_VLAN
30000:  from all fwmark 0x10000/0xff0000 lookup pbr_PLUSNET_WAN
32766:  from all lookup main
32767:  from all lookup default

Error: argument "al" is wrong: table id value is invalid

You missed an l in the command.

In the meantime, stop the pbr service to see if it makes a difference.

service pbr stop; service network restart; fw4 restart

You're right. I did! Sorry.

root@router:~# ip -4 addr; echo ""; ip ru; echo ""; ip -4 ro li ta all
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
9: br-lan.1@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.76.1/24 brd 192.168.76.255 scope global br-lan.1
       valid_lft forever preferred_lft forever
10: br-lan.20@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.20.1/24 brd 192.168.20.255 scope global br-lan.20
       valid_lft forever preferred_lft forever
11: br-lan.30@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.30.1/24 brd 192.168.30.255 scope global br-lan.30
       valid_lft forever preferred_lft forever
12: br-lan.40@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.40.1/24 brd 192.168.40.255 scope global br-lan.40
       valid_lft forever preferred_lft forever
13: br-lan.50@br-lan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet 192.168.50.1/24 brd 192.168.50.255 scope global br-lan.50
       valid_lft forever preferred_lft forever
14: WGVPN_IN_VLAN: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.76.0.1/24 brd 10.76.0.255 scope global WGVPN_IN_VLAN
       valid_lft forever preferred_lft forever
15: WGVPN_OUT_VLAN: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 10.14.0.2/16 brd 10.14.255.255 scope global WGVPN_OUT_VLAN
       valid_lft forever preferred_lft forever
16: pppoe-PLUSNET_W: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UNKNOWN group default qlen 3
    inet x.x.x.x peer 195.166.x.x/32 scope global pppoe-PLUSNET_W
       valid_lft forever preferred_lft forever

0:      from all lookup local
29997:  from all sport 51820 lookup pbr_PLUSNET_WAN
29998:  from all fwmark 0x20000/0xff0000 lookup pbr_WGVPN_OUT_VLAN
30000:  from all fwmark 0x10000/0xff0000 lookup pbr_PLUSNET_WAN
32766:  from all lookup main
32767:  from all lookup default

default via 195.166.x.x dev pppoe-PLUSNET_W table pbr_PLUSNET_WAN
10.76.0.0/24 dev WGVPN_IN_VLAN table pbr_PLUSNET_WAN proto kernel scope link src 10.76.0.1
192.168.20.0/24 dev br-lan.20 table pbr_PLUSNET_WAN proto kernel scope link src 192.168.20.1
192.168.30.0/24 dev br-lan.30 table pbr_PLUSNET_WAN proto kernel scope link src 192.168.30.1
192.168.40.0/24 dev br-lan.40 table pbr_PLUSNET_WAN proto kernel scope link src 192.168.40.1
192.168.50.0/24 dev br-lan.50 table pbr_PLUSNET_WAN proto kernel scope link src 192.168.50.1
192.168.76.0/24 dev br-lan.1 table pbr_PLUSNET_WAN proto kernel scope link src 192.168.76.1
default via 10.14.0.2 dev WGVPN_OUT_VLAN table pbr_WGVPN_OUT_VLAN
default via 195.166.130.251 dev pppoe-PLUSNET_W proto static
10.14.0.0/16 dev WGVPN_OUT_VLAN proto kernel scope link src 10.14.0.2
10.76.0.0/24 dev WGVPN_IN_VLAN proto kernel scope link src 10.76.0.1
138.199.29.202 via 195.166.x.x dev pppoe-PLUSNET_W proto static
192.168.20.0/24 dev br-lan.20 proto kernel scope link src 192.168.20.1
192.168.30.0/24 dev br-lan.30 proto kernel scope link src 192.168.30.1
192.168.40.0/24 dev br-lan.40 proto kernel scope link src 192.168.40.1
192.168.50.0/24 dev br-lan.50 proto kernel scope link src 192.168.50.1
192.168.76.0/24 dev br-lan.1 proto kernel scope link src 192.168.76.1
195.166.x.x dev pppoe-PLUSNET_W proto kernel scope link src x.x.x.x
local 10.14.0.2 dev WGVPN_OUT_VLAN table local proto kernel scope host src 10.14.0.2
broadcast 10.14.255.255 dev WGVPN_OUT_VLAN table local proto kernel scope link src 10.14.0.2
local 10.76.0.1 dev WGVPN_IN_VLAN table local proto kernel scope host src 10.76.0.1
broadcast 10.76.0.255 dev WGVPN_IN_VLAN table local proto kernel scope link src 10.76.0.1
local x.x.x.x dev pppoe-PLUSNET_W table local proto kernel scope host src x.x.x.x
local 127.0.0.0/8 dev lo table local proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo table local proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo table local proto kernel scope link src 127.0.0.1
local 192.168.20.1 dev br-lan.20 table local proto kernel scope host src 192.168.20.1
broadcast 192.168.20.255 dev br-lan.20 table local proto kernel scope link src 192.168.20.1
local 192.168.30.1 dev br-lan.30 table local proto kernel scope host src 192.168.30.1
broadcast 192.168.30.255 dev br-lan.30 table local proto kernel scope link src 192.168.30.1
local 192.168.40.1 dev br-lan.40 table local proto kernel scope host src 192.168.40.1
broadcast 192.168.40.255 dev br-lan.40 table local proto kernel scope link src 192.168.40.1
local 192.168.50.1 dev br-lan.50 table local proto kernel scope host src 192.168.50.1
broadcast 192.168.50.255 dev br-lan.50 table local proto kernel scope link src 192.168.50.1
local 192.168.76.1 dev br-lan.1 table local proto kernel scope host src 192.168.76.1
broadcast 192.168.76.255 dev br-lan.1 table local proto kernel scope link src 192.168.76.1

In the meantime, I've had a thought after you mentioned pbr. All my routing was set up with my DNS server in DNS ZONE. Now each client is being given a DNS server in its own zone, so the routing probably doesn't work. I'm going to try getting each client to use 192.168.50.1 as its DNS server (ie the version of AGH in the DNS zone instead of the version of AGH in its own zone). That way all the routing should still work....