Summary: GeoIP source and destination match on the same firewall/policy-route rule silently collapse onto a single shared nftables set, so one side's country/ASN list is discarded.
What were you attempting to achieve?
Configure a firewall rule that restricts traffic based on GeoIP independently on both the source and destination address — e.g. "source must be in the US, destination must be in China."
Configuration used
set firewall ipv4 name WAN-IN default-action drop
set firewall ipv4 name WAN-IN rule 10 action accept
set firewall ipv4 name WAN-IN rule 10 source geoip country-code us
set firewall ipv4 name WAN-IN rule 10 destination geoip country-code cn
set interfaces ethernet eth0 firewall in name WAN-IN
commit
What did you expect to happen?
Two independent nftables sets, one per side, each populated with the correct country's IP ranges, and the generated rule matching ip saddr @<us-set> ip daddr @<cn-set>.
What actually happens
Both sides compute the identical nftables set name, GEOIP_CC_name_WAN-IN_10 (see root cause below). Only one side's IP ranges end up in that set — whichever is processed last by geoip_update() — and the generated rule ends up matching ip saddr @GEOIP_CC_name_WAN-IN_10 ip daddr @GEOIP_CC_name_WAN-IN_10, i.e. both source and destination are checked against the same single country's ranges, silently discarding the other side's intended restriction. There is no error at commit time or at runtime; the misconfiguration is invisible from the CLI.
Verify with:
show configuration commands | match geoip
sudo nft list ruleset | grep -A5 'set GEOIP_CC_name_WAN-IN_10'
— only one country's ranges will be present in the set body, and the rule references that same set for both saddr and daddr.
Root cause
GEOIP_{CC|ASN}{6}_{hook}_{name}_{rule} set names, built independently in python/vyos/firewall.py:262 (rule match expression) and python/vyos/geoip.py (geoip_update(), lines 269/275/285/291 — the actual set contents), never include a source/destination discriminator. The sibling FQDN-match code three lines earlier in firewall.py (line 239) does append a _{prefix} (s/d) suffix for exactly this reason — the GeoIP branch appears to have been copy-pasted from it and the suffix dropped. See patch.md for the full trace and a proposed fix.
Environment
VyOS version: rolling (any recent build — verified by direct source inspection of the rolling branch, not tied to a specific dated build)
Reproduces on both ipv4 and ipv6 firewall rulesets, and on policy route / policy route6 rules (shares the same parse_rule() code path)
Prior art (checked on vyos.dev before filing)
No existing task describes this collision (searched ~30 GeoIP-related tasks). T7552 (closed, Invalid — a case-sensitivity mistake) confirms there's no existing validation that would prevent configuring GeoIP on both sides of one rule, so this is genuinely reachable today.