Well, I spent many (way too many!) hours researching and exploring different solutions before posting here, but wouldn't you know, I found my own answer less than an hour later. Intika's answer to a different question finally headed me in the right direction. I'm sure there are solutions with network namespaces, etc., but this was by far the simplest way to achieve our desired end.
iptables -t mangle -A OUTPUT -m owner --gid-owner "students" -j MARK --set-mark 42
iptables -t nat -A POSTROUTING -o enp1s0.20 -m mark --mark 42 -j SNAT --to-source 192.168.20.10
ip rule add fwmark 42 table 42
ip route add default via 192.168.20.2 dev enp1s0.20 table 42
netfilter-persistent save
The vlan IP is 192.168.20.10; the filtered gateway is 192.168.20.2.
NOTE 1: iptables-save does not make this persistent over reboots; only netfilter-persistent will save the iptables changes.
NOTE 2: This doesn't save the ip route configuration. This needs to be set to reload any time the interface is reactivated (unplugged cable, reboot, etc.). To make this configuration persistent, you need to configure the appropriate file in /etc/netplan. Since I wasn't able to parse the syntax or find relevant examples, I've appended the original iptables -j DROP
rule and saved this (with netfilter-persistent) so that in the event of an unattended network reset, all student access will be blocked. Then I've written a very simple script removing that line from the Filter table, and adding the ip route
line again.
#!/bin/bash
# Remove the permanent block on Group 'students' from the Filter table: sudo iptables -D OUTPUT -p tcp -m multiport --dport 80,443 -m owner --gid-owner "students" -j DROP
# Add the route policies that don't persist over a network restart without editing the netplan config:
sudo ip route add default via 192.168.20.2 dev enp1s0.20 table 42
echo "Filtered Access should be configured for Group 'students'"
exit