{"record":{"id":"c4149a782a828bad","repo":"jdx/mise","slug":"firewall-rule-name-mixes-ipv4-and-ipv6-source","errorCode":null,"errorMessage":"firewall rule '{name}' mixes IPv4 and IPv6 source/destination networks","messagePattern":"firewall rule '(.+?)' mixes IPv4 and IPv6 source/destination networks","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/firewall.rs","lineNumber":413,"sourceCode":"                .interface\n                .map(|interface| validate_interface(interface.trim()))\n                .transpose()?;\n            let source = rule\n                .source\n                .map(|source| source.parse::<IpNet>())\n                .transpose()\n                .wrap_err_with(|| format!(\"firewall rule '{name}' has an invalid source\"))?;\n            let destination = rule\n                .destination\n                .map(|destination| destination.parse::<IpNet>())\n                .transpose()\n                .wrap_err_with(|| format!(\"firewall rule '{name}' has an invalid destination\"))?;\n            if source.is_some_and(|source| {\n                destination.is_some_and(|destination| {\n                    source.addr().is_ipv4() != destination.addr().is_ipv4()\n                })\n            }) {\n                bail!(\"firewall rule '{name}' mixes IPv4 and IPv6 source/destination networks\");\n            }\n            let port = rule.port.map(FirewallPort::from_toml).transpose()?;\n            if port.is_some() && rule.protocol.is_none() {\n                bail!(\"firewall rule '{name}' sets port without protocol\");\n            }\n            rules.push(FirewallRule {\n                name,\n                state: rule.state,\n                direction: rule.direction,\n                action: rule.action,\n                port,\n                protocol: rule.protocol,\n                source,\n                destination,\n                interface,\n            });\n        }\n        let ssh_connection = std::env::var(\"SSH_CONNECTION\")","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/system/firewall.rs#L395-L431","documentation":"A firewall rule may filter on both `source` and `destination` CIDR networks, but both must belong to the same IP family. When one side is IPv4 and the other IPv6 (source.addr().is_ipv4() differs), the match cannot be expressed as a single backend rule and config parsing aborts with this error. The fields themselves parsed fine — only the family combination is rejected.","triggerScenarios":"A rule with `source = \"192.168.1.0/24\"` and `destination = \"fd00::/64\"` (or the reverse) — both parse as IpNet, then the `is_ipv4()` comparison fails. Common on dual-stack rules copied from nftables examples.","commonSituations":"Dual-stack hosts where the LAN is IPv4 but services sit on IPv6 (or vice versa); editing only one address when adapting an example rule; leftover ULA default (fd00::/8) combined with private IPv4 ranges.","solutions":["Make both sides the same family: change the destination to an IPv4 net, or the source to an IPv6 net.","Split into two rules — one IPv4-only, one IPv6-only — each with matching families.","Omit `source` or `destination` when you only need to match one side (a missing side matches any address)."],"exampleFix":"# before\n[[bootstrap.linux.firewall.rules]]\nname = \"lan-to-services\"\nsource = \"192.168.1.0/24\"\ndestination = \"fd00::/64\"\nport = 8443\nprotocol = \"tcp\"\n\n# after\n[[bootstrap.linux.firewall.rules]]\nname = \"lan-to-services-v4\"\nsource = \"192.168.1.0/24\"\ndestination = \"10.0.0.0/8\"\nport = 8443\nprotocol = \"tcp\"\n\n[[bootstrap.linux.firewall.rules]]\nname = \"lan-to-services-v6\"\nsource = \"fd00:ab::/48\"\ndestination = \"fd00::/64\"\nport = 8443\nprotocol = \"tcp\"","handlingStrategy":"validation","validationCode":"# pre-flight: source and destination must share an IP family\npython3 - <<'PY'\nimport tomllib, ipaddress\nfw = tomllib.load(open('mise.toml','rb')).get('bootstrap',{}).get('linux',{}).get('firewall',{})\nfor r in fw.get('rules',[]):\n    s, d = r.get('source'), r.get('destination')\n    if s and d and ipaddress.ip_network(s).version != ipaddress.ip_network(d).version:\n        raise SystemExit(f\"rule {r['name']}: mixed families {s} vs {d}\")\nPY","typeGuard":null,"tryCatchPattern":null,"preventionTips":["On dual-stack hosts, author one rule per family instead of one mixed rule.","When adapting example rules, change both sides or drop one.","Let a config linter compare ip_network().version on each pair."],"tags":["mise","firewall","network","ipv6","config","bootstrap"],"backgroundTag":"ip-address-family-mismatch","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}