Задача знакомая: у сайта есть серверы в нескольких странах, и нужно, чтобы клиент из России получал IP российского сервера, а клиент из Германии — европейского. Самый простой способ сделать это на своём DNS — BIND с поддержкой GeoIP2. Патчить BIND, как в 2010-х, больше не нужно. Скрипты, которые превращали CSV от MaxMind в огромные ACL, тоже не нужны.
Начиная с BIND 9.16 сервер умеет читать базы MaxMind в формате .mmdb напрямую. В ACL пишется geoip country RU;, ACL подставляется в match-clients у view, и каждая view отдаёт свою версию зоны. Пакеты BIND в Debian, Ubuntu и AlmaLinux уже собраны с библиотекой libmaxminddb. Остаётся скачать бесплатную базу GeoLite2 и настроить её обновление.
Главное ограничение надо понимать сразу. Авторитетный BIND видит IP не пользователя, а его рекурсивного резолвера. Обычно это резолвер провайдера в той же стране, поэтому схема работает. Но для клиентов публичных DNS и корпоративных VPN страна может определиться неверно. Подробно об этом — в разделе про подводные камни.
Как это устроено
Схема состоит из трёх частей:
- База GeoLite2-Country — файл
GeoLite2-Country.mmdbот MaxMind. Связывает IP-сети со странами и континентами. - ACL с элементами geoip — списки вида «все адреса из России, Беларуси и Казахстана».
- Views — несколько «представлений» одного DNS-сервера. Каждая view описывает, каким клиентам она отвечает (
match-clients), и содержит свои файлы зон.
BIND проверяет views сверху вниз и берёт первую подходящую. Поэтому порядок важен: частные случаи выше, «все остальные» (any) — последней.
Версии и пакеты
| Система | Пакет BIND | Версия BIND | Конфигурация | Служба |
|---|---|---|---|---|
| Debian 12 (bookworm) | bind9 | 9.18 | /etc/bind/ | named |
| Debian 13 (trixie) | bind9 | 9.20 | /etc/bind/ | named |
| Ubuntu 24.04 LTS | bind9 | 9.18 | /etc/bind/ | named |
| AlmaLinux / Rocky 9 | bind или bind9.18 | 9.16 или 9.18 | /etc/named.conf, зоны в /var/named/ | named |
| AlmaLinux / Rocky 10 | bind | 9.18 | /etc/named.conf, зоны в /var/named/ | named |
Во всех этих сборках GeoIP2 включён. Проверить можно так:
named -V | grep -i maxmind
В выводе должны быть строки с --with-maxminddb или упоминание libmaxminddb. Если пусто, BIND собран без GeoIP2, и директивы geoip в конфиге приведут к ошибке.
Установка, если BIND ещё не стоит:
# Debian / Ubuntu
apt install bind9 bind9-utils bind9-dnsutils
# AlmaLinux / Rocky
dnf install bind bind-utils
На AlmaLinux 9 базовый пакет bind — это 9.16, у которой закончилась поддержка ISC. Для нового сервера разумнее взять параллельный пакет bind9.18 из AppStream (утилиты — bind9.18-utils).
Шаг 1. Аккаунт MaxMind и ключ
С конца 2019 года GeoLite2 бесплатно, но только после регистрации и принятия лицензии. Порядок такой:
- Зарегистрироваться на сайте MaxMind в программе GeoLite (страница GeoLite2 Sign Up).
- В личном кабинете открыть раздел Manage License Keys и создать ключ.
- Записать Account ID и License Key. Ключ показывается один раз.
Лицензия GeoLite2 требует регулярно обновлять базу. Да и устаревшая база ошибается всё сильнее: адреса постоянно переходят между провайдерами и странами.
Шаг 2. Установка geoipupdate
geoipupdate — официальная утилита MaxMind. Она скачивает базы по ключу и атомарно подменяет файлы.
Debian и Ubuntu
В Debian пакет лежит в разделе contrib, в Ubuntu — в universe. В Debian добавьте contrib в источники (в Debian 12 — строка deb ... bookworm main contrib в /etc/apt/sources.list, в Debian 13 — Components: main contrib в /etc/apt/sources.list.d/debian.sources), затем:
apt update
apt install geoipupdate mmdb-bin
Важная деталь: пакет Debian и Ubuntu складывает базы в /var/lib/GeoIP, а не в /usr/share/GeoIP. Это пригодится при настройке BIND и AppArmor.
AlmaLinux и Rocky Linux
В AppStream и EPEL для 9 и 10 пакета geoipupdate нет. Берите RPM с официальной страницы релизов MaxMind на GitHub (github.com/maxmind/geoipupdate/releases):
dnf install https://github.com/maxmind/geoipupdate/releases/download/vX.Y.Z/geoipupdate_X.Y.Z_linux_amd64.rpm
dnf install libmaxminddb # в пакете есть утилита mmdblookup
Вместо X.Y.Z подставьте актуальную версию со страницы релизов. Этот пакет по умолчанию пишет базы в /usr/share/GeoIP.
Настройка /etc/GeoIP.conf
AccountID 123456
LicenseKey ваш_ключ
EditionIDs GeoLite2-Country
Для DNS по странам хватает базы Country. Базу City добавляйте, только если нужны регионы или города: она в разы больше и дольше грузится. Файл содержит ключ, поэтому закройте его от чтения посторонними:
chmod 600 /etc/GeoIP.conf
geoipupdate -v
ls -l /var/lib/GeoIP/ # Debian/Ubuntu
ls -l /usr/share/GeoIP/ # AlmaLinux
Проверьте, что база отвечает:
mmdblookup --file /var/lib/GeoIP/GeoLite2-Country.mmdb --ip 77.88.8.8 country iso_code
Примерно так выглядит ответ:
"RU" <utf8_string>
Шаг 3. Автоматическое обновление базы
В Debian и Ubuntu пакет приносит таймер systemd geoipupdate.timer (раз в неделю) и запасной cron. Таймер срабатывает, только если в /etc/GeoIP.conf заполнен AccountID. Включите и проверьте его:
systemctl enable --now geoipupdate.timer
systemctl list-timers geoipupdate.timer
Для RPM от MaxMind таймера нет, добавьте задание сами. Ниже — cron, который заодно перечитывает конфигурацию BIND. Без rndc reconfig или rndc reload BIND продолжит работать со старой копией базы: файлы открываются при старте и при перечитывании конфигурации.
# /etc/cron.d/geoipupdate-bind
30 5 * * 3,6 root /usr/bin/geoipupdate && /usr/sbin/rndc reconfig
В Debian можно оставить штатный таймер и добавить перечитывание через drop-in:
systemctl edit geoipupdate.service
[Service]
ExecStartPost=-/usr/sbin/rndc reconfig
Учтите, что в штатном юните включена жёсткая изоляция (ProtectSystem=strict, PrivateUsers=true). Если rndc из него не сможет прочитать /etc/bind/rndc.key, проще оставить таймер для скачивания и повесить rndc reconfig отдельным заданием cron через полчаса после него.
Шаг 4. Каталог с базами для BIND
По умолчанию BIND ищет базы в каталоге, который зависит от сборки libmaxminddb. Для пакетов дистрибутивов это /usr/share/GeoIP. Надёжнее указать путь явно в options.
BIND сам ищет в этом каталоге файлы с фиксированными именами: GeoIP2-Country.mmdb или GeoLite2-Country.mmdb, GeoIP2-City.mmdb или GeoLite2-City.mmdb, GeoIP2-ASN.mmdb или GeoLite2-ASN.mmdb, а также GeoIP2-ISP.mmdb и GeoIP2-Domain.mmdb. Переименовывать файлы нельзя.
Debian и Ubuntu: AppArmor
Профиль AppArmor для named разрешает читать только /usr/share/GeoIP/. Базы от geoipupdate лежат в /var/lib/GeoIP/, и без правки профиля BIND их не откроет. Перенастраивать geoipupdate на /usr/share/GeoIP не стоит: юнит systemd разрешает ему запись только в /var/lib/GeoIP. Правильный путь — локальное дополнение профиля:
echo '/var/lib/GeoIP/** r,' >> /etc/apparmor.d/local/usr.sbin.named
apparmor_parser -r /etc/apparmor.d/usr.sbin.named
Отключать AppArmor целиком ради этого не нужно.
AlmaLinux: SELinux
Файлы в /usr/share/GeoIP получают обычный контекст usr_t, и named их читает. Если положить базы в нестандартное место, проверьте отказы командой ausearch -m avc -ts recent и поправьте контекст через semanage fcontext и restorecon. Выключать SELinux не нужно.
Шаг 5. Конфигурация BIND с views
Пример: домен example.ru, три варианта зоны — для России и соседних стран, для Европы и для всех остальных.
Файлы зон
Делаем три копии зоны, которые отличаются только адресами нужных записей. Остальные записи (MX, TXT, NS) должны совпадать, иначе почта и проверки домена начнут вести себя по-разному в разных странах.
; /etc/bind/zones/example.ru.ru (на AlmaLinux: /var/named/example.ru.ru)
$TTL 300
@ IN SOA ns1.example.ru. hostmaster.example.ru. (
2026100301 ; serial
3600 ; refresh
900 ; retry
1209600 ; expire
300 ) ; negative TTL
IN NS ns1.example.ru.
IN NS ns2.example.ru.
ns1 IN A 192.0.2.10
ns2 IN A 198.51.100.10
@ IN A 192.0.2.50
www IN A 192.0.2.50
В example.ru.eu и example.ru.other у @ и www свои адреса, остальное то же. TTL для «географических» записей держите небольшим (60–300 секунд): так легче переключать трафик и исправлять ошибки.
Debian и Ubuntu
В Debian главный файл /etc/bind/named.conf подключает три файла: named.conf.options, named.conf.local и named.conf.default-zones. Последний объявляет служебные зоны (localhost, обратные зоны) вне views. Как только в конфигурации появляется хотя бы одна view, все зоны обязаны быть внутри views, и BIND не запустится. Поэтому строку include "/etc/bind/named.conf.default-zones"; из named.conf убираем, а служебные зоны при необходимости подключаем внутри последней view.
В /etc/bind/named.conf.options:
options {
directory "/var/cache/bind";
geoip-directory "/var/lib/GeoIP";
recursion no;
allow-query { any; };
allow-transfer { none; };
listen-on { any; };
listen-on-v6 { any; };
};
В /etc/bind/named.conf.local:
acl "geo-ru" {
geoip country RU;
geoip country BY;
geoip country KZ;
geoip country AM;
};
acl "geo-eu" {
geoip continent EU;
};
view "ru" {
match-clients { geo-ru; };
zone "example.ru" {
type primary;
file "/etc/bind/zones/example.ru.ru";
};
};
view "eu" {
match-clients { geo-eu; };
zone "example.ru" {
type primary;
file "/etc/bind/zones/example.ru.eu";
};
};
view "other" {
match-clients { any; };
zone "example.ru" {
type primary;
file "/etc/bind/zones/example.ru.other";
};
};
Порядок views здесь важен. Россия по базе MaxMind относится к континенту EU. Если поставить view eu первой, российские клиенты попадут в неё. Поэтому ru идёт выше eu.
Ключевые слова primary и secondary работают с BIND 9.16. Старые master и slave тоже принимаются, но в новых конфигах лучше использовать новые названия.
AlmaLinux и Rocky Linux
Здесь всё в одном файле /etc/named.conf. Стандартный файл содержит zone "." IN { type hint; ... } и include "/etc/named.rfc1912.zones"; вне views — их тоже нужно убрать или перенести внутрь view. Для чисто авторитетного сервера с recursion no зона корневых подсказок не нужна.
options {
listen-on port 53 { any; };
listen-on-v6 port 53 { any; };
directory "/var/named";
geoip-directory "/usr/share/GeoIP";
recursion no;
allow-query { any; };
allow-transfer { none; };
pid-file "/run/named/named.pid";
};
acl "geo-ru" { geoip country RU; geoip country BY; geoip country KZ; };
acl "geo-eu" { geoip continent EU; };
view "ru" {
match-clients { geo-ru; };
zone "example.ru" { type primary; file "example.ru.ru"; };
};
view "eu" {
match-clients { geo-eu; };
zone "example.ru" { type primary; file "example.ru.eu"; };
};
view "other" {
match-clients { any; };
zone "example.ru" { type primary; file "example.ru.other"; };
};
Файлы зон кладём в /var/named/ с владельцем root:named и правами 640. Если копировали файлы из другого места, восстановите контекст SELinux: restorecon -Rv /var/named. Откройте порт в firewalld: firewall-cmd --permanent --add-service=dns && firewall-cmd --reload.
Что ещё умеет geoip в ACL
Общий синтаксис: geoip [db база] поле значение;. Полезные поля:
country— двухбуквенный код ISO 3166-1 (RU,DE) или полное английское название в кавычках;continent— код континента (EU,AS,NA);region,city— нужна база City;asnum— номер автономной системы ("AS13238"или просто число), нужна база ASN.
Поля country и continent есть и в базе Country, и в City. Если установлены обе, BIND сначала смотрит City. Чтобы указать базу явно, используйте geoip db country country RU;.
Шаг 6. Проверка конфигурации и запуск
named-checkconf
named-checkzone example.ru /etc/bind/zones/example.ru.ru
systemctl restart named
journalctl -u named -n 50 --no-pager
В журнале должно появиться примерно так:
opened GeoIP2 database '/var/lib/GeoIP/GeoLite2-Country.mmdb'
Если строки нет, BIND не нашёл базу или не смог её прочитать. В этом случае ACL с geoip ни с кем не совпадут, и все клиенты попадут в последнюю view.
Шаг 7. Проверка ответов из разных стран
Локальный запрос с самого сервера ничего не скажет: адрес 127.0.0.1 не принадлежит ни одной стране и попадёт в other. Способы проверки:
- Запрос с серверов в разных странах. Если у вас есть VPS в России и в Европе, выполните на каждом:
dig @ns1.example.ru www.example.ru A +short +norecurse
Ответы должны отличаться. - Тестовая ACL. Временно добавьте в начало
match-clientsнужной view адрес своей рабочей машины, напримерmatch-clients { 203.0.113.25; geo-eu; };, выполнитеrndc reconfigи проверьте ответ. После проверки уберите адрес. - Исходящий адрес на сервере с несколькими IP. Ключ
dig -b адресзадаёт адрес отправителя. Так удобно проверять, если на другой машине есть IP из разных сетей. - Журнал запросов. Включите
rndc querylog on, сделайте запросы и посмотрите журнал. В строке запроса указана view, в которую попал клиент, примерно так:client @0x7f... 203.0.113.25#41522 (www.example.ru): view eu: query: www.example.ru IN A -E(0) (192.0.2.10)
После проверки выключите журнал:rndc querylog off. На нагруженном сервере он быстро раздувает логи.
Чтобы понять, какую страну база присвоила конкретному адресу, спросите её напрямую через mmdblookup, как в шаге 2. Так легко отличить ошибку конфигурации от ошибки базы.
Вторичный DNS-сервер и views
У зоны должно быть минимум два NS. С views это главный источник проблем. Вторичный сервер запрашивает передачу зоны со своего IP, попадает в одну view по своей стране и получает только её вариант зоны. Остальные варианты он не увидит.
Стандартное решение — отдельный ключ TSIG на каждую view. Вторичный сервер запрашивает каждую копию зоны со своим ключом, а первичный по ключу выбирает view:
key "xfr-ru" { algorithm hmac-sha256; secret "..."; };
key "xfr-eu" { algorithm hmac-sha256; secret "..."; };
key "xfr-other" { algorithm hmac-sha256; secret "..."; };
view "ru" {
match-clients { key xfr-ru; !key xfr-eu; !key xfr-other; geo-ru; };
allow-transfer { key xfr-ru; };
...
};
Ключи создаются командой tsig-keygen xfr-ru. На вторичном сервере такие же views с такими же geoip ACL, а в каждой зоне указан первичный сервер с нужным ключом (primaries { 192.0.2.10 key xfr-ru; };). TSIG проверяет время, поэтому часы на обоих серверах должны быть синхронизированы — подробнее в статье про синхронизацию времени в виртуальных машинах.
Если вторичные серверы ваши и их немного, иногда проще держать на каждом полную копию конфигурации и раскатывать зоны через git или rsync, без AXFR.
Подводные камни
BIND видит резолвер, а не клиента
Запрос к авторитетному серверу приходит от рекурсивного резолвера. Для клиентов провайдеров страна резолвера и клиента обычно совпадает. Для публичных резолверов (Google Public DNS, Cloudflare, Яндекс DNS), корпоративных сетей и VPN — не всегда.
Расширение EDNS Client Subnet (ECS, RFC 7871) передаёт часть адреса клиента, но open source BIND в авторитетном режиме его не использует для выбора view. Решение принимается по адресу резолвера. Если для вас критична точность для пользователей публичных DNS, смотрите на PowerDNS (ниже).
Кэш резолверов
Резолвер кэширует ответ на время TTL и отдаёт его всем своим клиентам. Если резолвер обслуживает пользователей из разных стран, все получат ответ для страны резолвера. После изменения адресов старые ответы живут до истечения TTL. Поэтому для гео-записей TTL — минуты, а не сутки.
Точность GeoLite2
Бесплатная база менее точна, чем платная GeoIP2. На уровне страны ошибки редки, но бывают: мобильные операторы, спутниковые и облачные сети, адреса, недавно переданные другому владельцу. Последняя view с any должна вести на рабочий сервер, а не на заглушку.
IPv6
База содержит и IPv6-сети, ACL работают для обоих протоколов. Не забудьте записи AAAA в каждой версии зоны, если серверы доступны по IPv6.
Открытый резолвер
Авторитетный сервер с views не должен заодно быть открытым рекурсивным резолвером. Держите recursion no. Если рекурсия нужна для своей сети, ограничьте её — см. статью про ограничение рекурсивных запросов в BIND.
Симптомы и решения
| Симптом | Причина | Решение |
|---|---|---|
| BIND не стартует: «when using ‘view’ statements, all zones must be in views» | Остались зоны вне views (в Debian — named.conf.default-zones, в AlmaLinux — named.rfc1912.zones и зона ".") | Убрать include или перенести зоны внутрь view |
Ошибка разбора конфига на строке с geoip | BIND собран без libmaxminddb | Проверить named -V, поставить пакет дистрибутива |
| Все клиенты получают ответ из последней view | База не открылась: неверный geoip-directory, имя файла или запрет AppArmor | Найти в журнале строку «opened GeoIP2 database», проверить journalctl -k | grep DENIED, поправить профиль |
| Россияне попадают в европейскую view | View с continent EU стоит выше view для RU | Поставить частные views выше общих |
| Вторичный NS отдаёт не тот вариант зоны | Передача зоны идёт через view по стране вторичного сервера | Ключи TSIG на каждую view |
| Ответы не меняются после обновления базы | BIND держит открытой старую копию | rndc reconfig после geoipupdate |
geoipupdate: ошибка 401 или «invalid license key» | Неверный AccountID или ключ, ключ отозван | Создать новый ключ в кабинете MaxMind |
| Пользователь из России получил европейский адрес | Он ходит через публичный резолвер или VPN за рубежом | Особенность схемы; для ECS — PowerDNS |
Альтернатива: PowerDNS с GeoIP-бэкендом
PowerDNS Authoritative Server умеет то же, но по-другому. Вместо копий зон — YAML-файл с «сервисами», где имя строится по шаблону из кода страны или континента. Главное отличие от BIND: PowerDNS умеет учитывать EDNS Client Subnet. Для этого включается параметр edns-subnet-processing=yes.
Пакеты: в Debian и Ubuntu — pdns-server и pdns-backend-geoip. Для AlmaLinux удобнее официальные репозитории PowerDNS (repo.powerdns.com). Фрагмент конфигурации:
# /etc/powerdns/pdns.conf
launch=geoip
geoip-database-files=/var/lib/GeoIP/GeoLite2-Country.mmdb
geoip-zones-file=/etc/powerdns/geo.yaml
edns-subnet-processing=yes
# /etc/powerdns/geo.yaml
domains:
- domain: geo.example.ru
ttl: 60
records:
geo.example.ru:
- soa: ns1.example.ru hostmaster.example.ru 2026100301 7200 3600 1209600 60
- ns: ns1.example.ru
- ns: ns2.example.ru
ru.www.geo.example.ru:
- a: 192.0.2.50
eu.www.geo.example.ru:
- a: 198.51.100.50
default.www.geo.example.ru:
- a: 203.0.113.50
services:
www.geo.example.ru: [ '%cc.www.geo.example.ru', '%cn.www.geo.example.ru', 'default.www.geo.example.ru' ]
Здесь %cc — код страны, %cn — код континента. PowerDNS перебирает варианты по порядку и берёт первое существующее имя. Подробности — в документации GeoIP-бэкенда PowerDNS. Там же описаны LUA-записи — ещё один способ гео-ответов, с проверкой доступности серверов.
| BIND + views | PowerDNS GeoIP | |
|---|---|---|
| Учёт ECS | Нет | Да |
| Как описываются варианты | Отдельная копия зоны на каждую view | Шаблоны имён в YAML |
| Вторичные серверы | AXFR через TSIG на каждую view | Бэкенд не работает как primary, на каждый сервер кладётся своя копия YAML |
| Порог входа | Ниже, если BIND уже стоит | Новый сервер и формат |
Если нужна не просто привязка к стране, а балансировка с проверкой здоровья серверов, рассмотрите управляемые DNS-сервисы с гео-маршрутизацией. Но это уже другой класс решений.
Частые вопросы
Нужен ли старый скрипт, который превращал базу MaxMind в ACL?
Нет. Старые CSV-базы GeoIP Legacy MaxMind больше не выпускает, а BIND с версии 9.16 читает .mmdb напрямую через geoip в ACL. Огромные ACL из тысяч сетей только замедляли загрузку конфигурации.
Можно ли обойтись без регистрации в MaxMind?
Для GeoLite2 — нет, нужен аккаунт и ключ. Есть альтернативные бесплатные базы в формате MMDB от других проектов, но BIND ищет файлы по именам GeoLite2/GeoIP2 и ожидает их структуру данных. Такую замену нужно отдельно проверять.
Как часто обновлять базу?
Достаточно раз-два в неделю. После каждого обновления выполняйте rndc reconfig, иначе BIND продолжит работать со старыми данными до перезапуска.
Работает ли DNSSEC с views?
Можно, но это отдельная задача. Каждая копия зоны подписывается внутри своей view, а в родительской зоне у домена одна запись DS. Значит, все копии должны подписываться одними и теми же ключами, иначе часть клиентов получит ошибку проверки подписи. Если DNSSEC не обязателен, запустите гео-DNS без него, а подпись добавьте позже, отработав её на тестовом домене.
Почему с моего компьютера ответ «не той» страны?
Скорее всего, запрос идёт через публичный DNS или VPN, и BIND видит адрес резолвера. Проверьте напрямую: dig @ns1.example.ru www.example.ru с вашей машины. Тогда BIND увидит ваш собственный адрес.
Можно ли выбирать view по провайдеру, а не по стране?
Да, через geoip asnum и базу GeoLite2-ASN. Добавьте её в EditionIDs, и BIND подхватит файл GeoLite2-ASN.mmdb из того же каталога.