GeoIP alias: Kosovo (XK) is downloaded from MaxMind but cannot be selected or saved ("not a valid country code")

Open Beginner friendly
#11,210 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
php, python

Research direction

Start with the two places that build the country list: AliasContentField::getCountryCodes() in src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php, which validates entries, and AliasController::listCountriesAction() in src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php, which feeds the dialog. Both special-case EU; add XK the same way and confirm XK is accepted and listed. Done when an alias with content XK validates and Kosovo appears under Europe in the GeoIP dialog.

Written by the indexing model from the issue text.

Description

Important notices

Describe the bug

The GeoIP downloader (scripts/filter/lib/alias/geoip.py) writes per-country files for every country code found in the MaxMind GeoLite2 Country CSV. Kosovo uses the user-assigned code XK, so after a normal download the files /usr/local/share/GeoIP/alias/XK-IPv4 (51 networks) and XK-IPv6 (58 networks) exist on the system.

Kosovo can't be used, though:

  • The country list in the alias dialog (AliasController::listCountriesAction()) is built from contrib/iana/tzdata-iso3166.tab, which does not contain XK.
  • Validation (AliasContentField::getCountryCodes()) uses the same file and rejects it: Entry "XK" is not a valid country code.

EU (MaxMind "Unclassified") is already special-cased in both places. XK is the only code present in the MaxMind data and missing in OPNsense:

OPNsense selectable countries: 249 | MaxMind countries in /usr/local/share/GeoIP/alias: 250
present in MaxMind data but not selectable: XK (IPv4 51, IPv6 58)

This was reported before in #9562 (Kosovo missing from the list). It was closed by the bot after 180 days without a fix. Back then the reporter noted that MaxMind had the same gap, but the current GeoLite2 data does contain Kosovo.

To Reproduce

  1. Configure a MaxMind GeoLite2 license and download the GeoIP data.
  2. ls /usr/local/share/GeoIP/alias/XK-* shows that the files exist.
  3. Firewall → Aliases → + → type GeoIP: Kosovo is not in the Europe list.
  4. Create the alias via API/model with content XK: Entry "XK" is not a valid country code.

Expected behavior

Kosovo can be selected in GeoIP aliases, like EU.

Proposed fix

Handle XK the same way as EU (patch below). The alias resolver (geoip.py iter_addresses()) already reads <country>-<proto> files generically, so no other change is needed.

--- a/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php
+++ b/src/opnsense/mvc/app/models/OPNsense/Firewall/FieldTypes/AliasContentField.php
@@ -105,6 +105,8 @@
         if (empty(self::$internalCountryCodes)) {
             // Maxmind's country code 6255148 (EU Unclassified)
             self::$internalCountryCodes[] = 'EU';
+            // Maxmind's user-assigned code for Kosovo (not part of ISO 3166 / tzdata)
+            self::$internalCountryCodes[] = 'XK';
             $contribDir = (new AppConfig())->application->contribDir;
--- a/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php
+++ b/src/opnsense/mvc/app/controllers/OPNsense/Firewall/Api/AliasController.php
@@ -248,6 +248,10 @@
         $result = [
             'EU' => [
                 'name' => gettext('Unclassified'),
+                'region' => 'Europe'
+            ],
+            'XK' => [
+                'name' => gettext('Kosovo'),
                 'region' => 'Europe'
             ]
         ];

Related observation: the EU special case seems to be obsolete. The current GeoLite2 Country CSV (GeoLite2-Country-CSV_20261113) has no location with country code EU any more (0 entries in GeoLite2-Country-Locations-en.csv), and no EU-IPv4/EU-IPv6 files are created. So "Unclassified (EU)" is still offered in the dialog, but an alias using it silently resolves to nothing.

php -l passes for both files. Current workaround: a static network alias filled from XK-IPv4/XK-IPv6, but that one does not update together with the GeoIP data.

Environment

OPNsense 26.7.9_1 (amd64), MaxMind GeoLite2 Country CSV (GeoLite2-Country-CSV_20261113, downloaded 2026-11-14)

AI disclosure

This report and the proposed fix were prepared with the help of an AI assistant (Claude). The analysis was done on my own OPNsense system: the file comparison, the CSV check, the validation error and the php -l check come from my firewall. I reviewed the report before submitting.

Dominant language
PHP
Stars
4.8k
Forks
1k
Avg merge
1d 20h
Merged PRs (30d)
36

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from opnsense/core

All issues in opnsense/core

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.