zapretgui/src/dns/dns_providers.py
loop-uh c3260118bd
All checks were successful
Zapret source guards / validate (push) Successful in 15s
Windows-сборка / Dev Dev 21.1.7.116 опубликован
Zapret DNS: новый адрес и проверка, что сервер доступен из сети пользователя
Прежний адрес 144.31.82.230 закрыли в России по IP. Плитка и профиль hosts
переведены на 83.217.211.149, старый адрес на адаптерах меняется на новый.
Перед сменой DNS и перед записью hosts программа проверяет, что сервер
отвечает из сети пользователя, и не ставит недоступный.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:57:21 +03:00

598 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# dns/dns_providers.py
"""
Список DNS провайдеров для UI
Значок ("icon") рисует profile.ui.profile_icon: кроме имён Font Awesome
("fa5s.*", "fa5b.*") подходят фирменные логотипы "simple:<имя>:<буквы>" и свои
SVG "own:<имя>:<буквы>". У серверов из раздела «Для ИИ» значок и цвет те же,
что у одноимённого DNS-профиля в «Редакторе hosts» (hosts/ui/profile_icons.py):
один сервер — один значок во всей программе.
"doh" — адрес шифрованного DNS поверх HTTPS, "dot" — имя сервера для
шифрованного DNS поверх TLS (порт 853): по этому имени проверяется его
сертификат. "dot" указан только там, где сервер действительно отвечает по
этому способу (проверено запросом 2026-10-07); у остальных проверка DoT не
делается, чтобы не выдавать «не поддерживает» за «заблокирован».
"encrypted_only": True — сервер принимает только шифрованные запросы (DoH,
DoT), а обычный DNS на порту 53 у него закрыт намеренно (проверено запросом
2026-10-08). Спрашивать такой сервер сама умеет только Windows 11, поэтому на
Windows без DoH плитка не применяется, а замер скорости и проверка обычного
DNS такие серверы пропускают: молчание порта 53 здесь не блокировка.
"dnssec": True — сервер проверяет подписи DNSSEC: на имя с заведомо
испорченной подписью (dnssec-failed.org) он отвечает отказом, а не адресом
(проверено запросом 2026-10-07). На плитке это метка «DNSSEC».
"status" — пометка состояния сервера в России (плитка показывает её цветом):
STATUS_BLOCKED — обычные запросы блокируются или перехватываются по дороге
(Google и Cloudflare — с августа 2026); STATUS_AT_RISK — пока работает, но уже
попадал под ограничения и может перестать отвечать. Пометка только
предупреждает: выбрать такой сервер по-прежнему можно.
Убранные серверы. dns.malw.link: оба опубликованных адреса не отвечают с начала
октября 2026 (так пишет и страница состояния самого сервиса), а оставшийся
узел отдаёт для сайтов ИИ адрес посредника, который тоже не работает. Вернуть
можно одной записью, когда сервис оживёт. Xbox DNS (все три плитки): владелец
2026-10-08 объявил, что гасит все сервисы, кроме обычного DNS и Xbox Live, и
на сайты ИИ сервер теперь отвечает пустым адресом (chatgpt.com → 0.0.0.0) —
то есть не открывает их, а ломает. Что стало с его адресами на адаптерах —
см. замены в конце файла.
Группа «Малоизвестные» — небольшие и нишевые серверы: из-за малой известности
они реже попадают под блокировки. Группа «Для ИИ» — серверы сообщества,
которые подменяют адреса закрытых для России сервисов; доверия к ним меньше,
чем к крупным компаниям, и страница пишет об этом рядом с названием группы.
Группа «Шифрованные» — не серверы, а режимы встроенного движка
dnscrypt-proxy (dns.local_proxy): ключ "local_proxy" называет режим, а адрес
у всех один — 127.0.0.1, сам этот компьютер. Выбор такой плитки сначала
запускает движок и только потом прописывает адаптеру 127.0.0.1. Замер
скорости, проверка серверов и поиск сервера по адресу эти записи пропускают
(network_providers()): какой режим работает, знает только сам движок.
Основные адреса (первый в "ipv4") не должны повторяться: по нему программа
узнаёт, какой сервер стоит на адаптере. Это и остальные правила списка
проверяет catalog_problems().
"""
from __future__ import annotations
import ipaddress
STATUS_BLOCKED = "blocked"
STATUS_AT_RISK = "at_risk"
STATUSES = (STATUS_BLOCKED, STATUS_AT_RISK)
LOCAL_PROXY_GROUP = "Шифрованные"
DNS_PROVIDERS = {
LOCAL_PROXY_GROUP: {
"DNSCrypt": {
"ipv4": ["127.0.0.1"],
"ipv6": ["::1"],
"desc": "Шифрует запросы",
"icon": "fa5s.key",
"color": "#22d3ee",
"dnssec": True,
"local_proxy": "dnscrypt",
},
"DNSCrypt анонимный": {
"ipv4": ["127.0.0.1"],
"ipv6": ["::1"],
"desc": "Скрывает ваш адрес",
"icon": "fa5s.user-secret",
"color": "#a3e635",
"dnssec": True,
"local_proxy": "anonymized",
},
"ODoH": {
"ipv4": ["127.0.0.1"],
"ipv6": ["::1"],
"desc": "Через HTTPS, пробный",
"icon": "fa5s.mask",
"color": "#f472b6",
"dnssec": True,
"local_proxy": "odoh",
},
},
"Популярные": {
"Cloudflare": {
"ipv4": ["1.1.1.1", "1.0.0.1"],
"ipv6": ["2606:4700:4700::1111", "2606:4700:4700::1001"],
"desc": "Быстрый и приватный",
"icon": "simple:cloudflare:CF",
"color": "#f48120",
"doh": "https://cloudflare-dns.com/dns-query",
"dot": "cloudflare-dns.com",
"dnssec": True,
"status": STATUS_BLOCKED,
},
"Google DNS": {
"ipv4": ["8.8.8.8", "8.8.4.4"],
"ipv6": ["2001:4860:4860::8888", "2001:4860:4860::8844"],
"desc": "Надёжный",
"icon": "simple:google:G",
"color": "#4285f4",
"doh": "https://dns.google/dns-query",
"dot": "dns.google",
"dnssec": True,
"status": STATUS_BLOCKED,
},
# Отвечает быстро и в России не блокируется, но это российский сервер:
# запрещённые в России сайты он может не отдавать.
"Яндекс DNS": {
"ipv4": ["77.88.8.8", "77.88.8.1"],
"ipv6": ["2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff"],
"desc": "Российский, быстрый",
"icon": "fa5b.yandex",
"color": "#fc3f1d",
"doh": "https://common.dot.dns.yandex.net/dns-query",
"dot": "common.dot.dns.yandex.net",
},
},
"Безопасные": {
"Quad9": {
"ipv4": ["9.9.9.9", "149.112.112.112"],
"ipv6": ["2620:fe::fe", "2620:fe::9"],
"desc": "Антивирус",
"icon": "simple:quad9:Q9",
"color": "#e91e63",
"doh": "https://dns.quad9.net/dns-query",
"dot": "dns.quad9.net",
"dnssec": True,
},
"AdGuard": {
"ipv4": ["94.140.14.14", "94.140.15.15"],
"ipv6": ["2a10:50c0::ad1:ff", "2a10:50c0::ad2:ff"],
"desc": "Без рекламы",
"icon": "simple:adguard:AG",
"color": "#68bc71",
"doh": "https://dns.adguard-dns.com/dns-query",
"dot": "dns.adguard-dns.com",
"dnssec": True,
"status": STATUS_AT_RISK,
},
"AdGuard Семейный": {
"ipv4": ["94.140.14.15", "94.140.15.16"],
"ipv6": ["2a10:50c0::bad1:ff", "2a10:50c0::bad2:ff"],
"desc": "Без рекламы и 18+",
"icon": "simple:adguard:AG",
"color": "#3f9d8c",
"doh": "https://family.adguard-dns.com/dns-query",
"dot": "family.adguard-dns.com",
"dnssec": True,
"status": STATUS_AT_RISK,
},
"OpenDNS": {
"ipv4": ["208.67.222.222", "208.67.220.220"],
"ipv6": ["2620:119:35::35", "2620:119:53::53"],
"desc": "Фильтрация",
"icon": "own:opendns:OD",
"color": "#fe7702",
"doh": "https://doh.opendns.com/dns-query",
"dot": "dns.opendns.com",
"dnssec": True,
"status": STATUS_AT_RISK,
},
"Яндекс Безопасный": {
"ipv4": ["77.88.8.88", "77.88.8.2"],
"ipv6": ["2a02:6b8::feed:bad", "2a02:6b8:0:1::feed:bad"],
"desc": "Антивирус, российский",
"icon": "fa5b.yandex",
"color": "#f5a623",
"doh": "https://safe.dot.dns.yandex.net/dns-query",
"dot": "safe.dot.dns.yandex.net",
},
"Яндекс Семейный": {
"ipv4": ["77.88.8.7", "77.88.8.3"],
"ipv6": ["2a02:6b8::feed:a11", "2a02:6b8:0:1::feed:a11"],
"desc": "Без 18+, российский",
"icon": "fa5b.yandex",
"color": "#7b61ff",
"doh": "https://family.dot.dns.yandex.net/dns-query",
"dot": "family.dot.dns.yandex.net",
},
# Российская компания по кибербезопасности; закрывает только вредоносные
# сайты, реестр блокировок не применяет (rutracker.org отдаёт настоящий адрес).
"BI.ZONE": {
"ipv4": ["185.191.32.7", "185.191.32.8"],
"ipv6": [],
"desc": "Антивирус, российский",
"icon": "fa5s.shield-virus",
"color": "#2dd4bf",
"doh": "https://public.sdns.bi.zone/9c0205cf-e32e-418e-adc0-e2575ebcf394",
"dot": "public.sdns.bi.zone",
"dnssec": True,
},
"DNS4EU Защитный": {
"ipv4": ["86.54.11.1", "86.54.11.201"],
"ipv6": ["2a13:1001::86:54:11:1", "2a13:1001::86:54:11:201"],
"desc": "Антивирус, Евросоюз",
"icon": "simple:europeanunion:EU",
"color": "#3b6fd4",
"doh": "https://protective.joindns4.eu/dns-query",
"dot": "protective.joindns4.eu",
"dnssec": True,
},
"CleanBrowsing": {
"ipv4": ["185.228.168.9", "185.228.169.9"],
"ipv6": ["2a0d:2a00:1::2", "2a0d:2a00:2::2"],
"desc": "Антивирус",
"icon": "fa5s.broom",
"color": "#38bdf8",
"doh": "https://doh.cleanbrowsing.org/doh/security-filter/",
"dot": "security-filter-dns.cleanbrowsing.org",
"dnssec": True,
},
"Control D Без рекламы": {
"ipv4": ["76.76.2.2", "76.76.10.2"],
"ipv6": ["2606:1a40::2", "2606:1a40:1::2"],
"desc": "Без рекламы и слежки",
"icon": "fa5s.ban",
"color": "#a78bfa",
"doh": "https://freedns.controld.com/p2",
"dnssec": True,
},
},
"Малоизвестные": {
"Dns.SB": {
"ipv4": ["185.222.222.222", "45.11.45.11"],
"ipv6": ["2a09::", "2a11::"],
"desc": "Без цензуры",
"icon": "fa5s.unlock-alt",
"color": "#00bcd4",
"doh": "https://doh.sb/dns-query",
"dot": "dot.sb",
"dnssec": True,
},
# Шаблон — с официального dnsdoh.art: DoH на обычном порту 443. Прежний
# порт 444 молчит (проверено 2026-10-08). Рекламные имена сервер отдаёт
# пустым адресом (doubleclick.net → 0.0.0.0), и отключить это нельзя.
"dnsdoh.art": {
"ipv4": ["194.180.189.33"],
"ipv6": [],
"desc": "Без рекламы и записи",
"icon": "fa5s.lock",
"color": "#9c27b0",
"doh": "https://dnsdoh.art/dns-query",
"dot": "dnsdoh.art",
"dnssec": True,
},
"Control D": {
"ipv4": ["76.76.2.0", "76.76.10.0"],
"ipv6": ["2606:1a40::", "2606:1a40:1::"],
"desc": "Без фильтров и записи",
"icon": "fa5s.sliders-h",
"color": "#8b5cf6",
"doh": "https://freedns.controld.com/p0",
"dnssec": True,
},
"DNS4EU": {
"ipv4": ["86.54.11.100", "86.54.11.200"],
"ipv6": ["2a13:1001::86:54:11:100", "2a13:1001::86:54:11:200"],
"desc": "Без фильтров, Евросоюз",
"icon": "simple:europeanunion:EU",
"color": "#f5c518",
"doh": "https://unfiltered.joindns4.eu/dns-query",
"dot": "unfiltered.joindns4.eu",
"dnssec": True,
},
# Имя для шифрования — из официальной справки docs.gcore.com; по нему
# отвечают оба адреса и по DoH, и по DoT (проверено 2026-10-08).
"Gcore": {
"ipv4": ["95.85.95.85", "2.56.220.2"],
"ipv6": ["2a03:90c0:999d::1", "2a03:90c0:9992::1"],
"desc": "Быстрый, без записи",
"icon": "simple:gcore:GC",
"color": "#ff4c00",
"doh": "https://gcoredns.com/dns-query",
"dot": "gcoredns.com",
},
"DNS.Watch": {
"ipv4": ["84.200.69.80", "84.200.70.40"],
"ipv6": ["2001:1608:10:25::1c04:b12f", "2001:1608:10:25::9249:d69b"],
"desc": "Без цензуры и записи",
"icon": "fa5s.eye-slash",
"color": "#4ade80",
"dnssec": True,
},
"UltraDNS": {
"ipv4": ["64.6.64.6", "64.6.65.6"],
"ipv6": ["2620:74:1b::1:1", "2620:74:1c::2:2"],
"desc": "Надёжный, без фильтров",
"icon": "fa5s.bolt",
"color": "#facc15",
"dnssec": True,
},
# Регистратор чешских доменов .cz.
"CZ.NIC": {
"ipv4": ["193.17.47.1", "185.43.135.1"],
"ipv6": ["2001:148f:ffff::1", "2001:148f:fffe::1"],
"desc": "Без фильтров, Чехия",
"icon": "fa5s.landmark",
"color": "#60a5fa",
"doh": "https://odvr.nic.cz/dns-query",
"dot": "odvr.nic.cz",
"dnssec": True,
},
# Регистратор канадских доменов .ca; режим Private — без фильтров.
"CIRA Canadian Shield": {
"ipv4": ["149.112.121.10", "149.112.122.10"],
"ipv6": ["2620:10a:80bb::10", "2620:10a:80bc::10"],
"desc": "Без фильтров, Канада",
"icon": "fa5b.canadian-maple-leaf",
"color": "#ef4444",
"doh": "https://private.canadianshield.cira.ca/dns-query",
"dot": "private.canadianshield.cira.ca",
"dnssec": True,
},
# Общественная сеть Freifunk München (Германия).
"Freifunk München": {
"ipv4": ["5.1.66.255", "185.150.99.255"],
"ipv6": ["2001:678:e68:f000::", "2001:678:ed0:f000::"],
"desc": "Без цензуры и записи",
"icon": "fa5s.broadcast-tower",
"color": "#ec4899",
"doh": "https://doh.ffmuc.net/dns-query",
"dot": "dot.ffmuc.net",
"dnssec": True,
},
# Некоммерческая организация из США, держит узлы сети I2P. Шифрованные
# запросы принимает только первый адрес: 23.128.248.4 отказывает по портам
# 443 и 853 (проверено 2026-10-08), обычный DNS отвечает на обоих.
"StormyCloud": {
"ipv4": ["23.128.248.2", "23.128.248.4"],
"ipv6": ["2602:fc05::2", "2602:fc05::4"],
"desc": "Без цензуры и записи",
"icon": "fa5s.cloud-rain",
"color": "#93c5fd",
"doh": "https://dns.stormycloud.org/dns-query",
"dnssec": True,
},
"dnsforge.de": {
"ipv4": ["176.9.93.198", "176.9.1.117"],
"ipv6": ["2a01:4f8:151:34aa::198", "2a01:4f8:141:316d::117"],
"desc": "Без рекламы и записи",
"icon": "fa5s.hammer",
"color": "#fb923c",
"doh": "https://dnsforge.de/dns-query",
"dot": "dnsforge.de",
"dnssec": True,
},
"Surfshark DNS": {
"ipv4": ["194.169.169.169"],
"ipv6": ["2a09:a707:169::"],
"desc": "Без записи запросов",
"icon": "simple:surfshark:SS",
"color": "#1ebfbf",
"doh": "https://dns.surfsharkdns.com/dns-query",
"dot": "dns.surfsharkdns.com",
},
# Сервер фонда «Викимедиа» (Википедия). Обычный DNS не принимает вовсе.
"Wikimedia DNS": {
"ipv4": ["185.71.138.138"],
"ipv6": ["2001:67c:930::1"],
"desc": "Без фильтров, Википедия",
"icon": "fa5b.wikipedia-w",
"color": "#94a3b8",
"doh": "https://wikimedia-dns.org/dns-query",
"dot": "wikimedia-dns.org",
"dnssec": True,
"encrypted_only": True,
},
},
"Для ИИ": {
# Свой сервер проекта в Нидерландах (с 2026-10-09; первый адрес
# 144.31.82.230 в тот же день закрыли в России по IP — см. замены ниже). Имена сервисов с
# гео-ограничением (ChatGPT, Claude, Gemini, Spotify и другие из каталога
# hosts) отдаёт со своим адресом и сам пересылает соединение на настоящий
# сайт; остальные имена — настоящий ответ, без фильтров. Список имён —
# /etc/zapret-dns/zones.txt на сервере. Шифрованный DNS — по имени
# dns.zapret.moe: DoH на общем порту 443, DoT на 853 (проверено 2026-10-09).
"Zapret DNS": {
"ipv4": ["83.217.211.149"],
"ipv6": ["2a03:afc0:9::235d"],
"desc": "Нейросети и Spotify",
"icon": "fa5s.rocket",
"color": "#60CDFF",
"doh": "https://dns.zapret.moe/dns-query",
"dot": "dns.zapret.moe",
"dnssec": True,
},
# Кроме подмены адресов сайтов ИИ, рекламные имена отдаёт пустым адресом
# (doubleclick.net → 0.0.0.0, проверено 2026-10-08).
"Comss DNS": {
"ipv4": ["83.220.169.155", "212.109.195.93"],
"ipv6": [],
"desc": "Нейросети, без рекламы",
"icon": "fa5s.shield-alt",
"color": "#2F80ED",
"doh": "https://dns.comss.one/dns-query",
"dot": "dns.comss.one"
},
# Адреса — у официального имени DoH dns.astracat.network: опубликованный
# на сайте 85.209.2.112 на обычные DNS-запросы не отвечает (проверено 2026-09-29).
"AstraCat": {
"ipv4": ["135.106.217.200", "135.106.197.22"],
"ipv6": [],
"desc": "ChatGPT, без рекламы",
"icon": "fa5s.cat",
"color": "#F59E0B",
"doh": "https://dns.astracat.network/dns-query",
"dot": "dns.astracat.network",
"dnssec": True,
},
# Российская пара из официального geohide.ru/static/metadata/servers.json.
# Подменяет ответы только для сайтов из своего списка (ChatGPT, Grok, Notion…).
"GeoHide": {
"ipv4": ["193.233.112.67", "193.233.112.68"],
"ipv6": [],
"desc": "ChatGPT, Grok, Notion",
"icon": "fa5s.globe-europe",
"color": "#8B5CF6",
"doh": "https://geohide.ru/dns-query",
"dot": "geohide.ru"
},
# Адреса и шаблон — с официального dns-ai.ru. Обычный DNS наружу не отдаёт.
# Адреса сайтов ИИ подменяет только для домашних сетей России и Беларуси
# (ChatGPT, Claude, Grok, Copilot, Manus — через свой узел, проверено из шести
# российских сетей 2026-10-08; его узел для Google в тот день не открывался).
# Рекламные имена отвечают пустым адресом (doubleclick.net → 0.0.0.0).
"DNS-AI": {
"ipv4": ["192.144.59.14", "186.246.49.127"],
"ipv6": ["2a0d:8480:0:67c::14", "2a0a:2b41:0:500d::53"],
"desc": "Нейросети, без рекламы",
"icon": "fa5s.robot",
"color": "#d946ef",
"doh": "https://dns.dns-ai.ru/dns-query",
"dot": "dns.dns-ai.ru",
"dnssec": True,
"encrypted_only": True,
},
}
}
def iter_providers():
"""(группа, название, данные) для каждого сервера списка."""
for group, providers in DNS_PROVIDERS.items():
for name, data in providers.items():
yield group, name, data
def network_providers(providers: dict | None = None) -> dict:
"""Список без режимов встроенного движка: только настоящие серверы в сети."""
source = DNS_PROVIDERS if providers is None else providers
result: dict = {}
for group, items in source.items():
kept = {name: data for name, data in items.items() if not data.get("local_proxy")}
if kept:
result[group] = kept
return result
def is_encrypted_only(data: dict) -> bool:
"""Сервер не отвечает на обычный DNS: спросить его можно только шифрованным запросом."""
return bool(data.get("encrypted_only"))
def find_provider_by_address(address: str) -> tuple[str, str, dict] | None:
"""(группа, название, данные) сервера, которому принадлежит адрес (IPv4 или IPv6)."""
wanted = str(address or "").strip().lower()
if not wanted:
return None
for group, name, data in iter_providers():
if data.get("local_proxy"):
continue
for item in (*data.get("ipv4", ()), *data.get("ipv6", ())):
if str(item).strip().lower() == wanted:
return group, name, data
return None
def catalog_problems() -> list[str]:
"""Нарушения правил списка; у исправного списка — пусто.
Проверяет то, от чего зависит работа программы: адреса — настоящие IPv4 и
IPv6 без повторов, основной адрес не занят другим сервером, шифрованный
DoH задан адресом https (у сервера только с шифрованием он обязателен),
пометка состояния — из известных.
"""
from dns.local_proxy_catalog import MODES
problems: list[str] = []
names: set[str] = set()
owners: dict[str, str] = {}
modes: set[str] = set()
for group, name, data in iter_providers():
if name in names:
problems.append(f"{name}: название повторяется")
names.add(name)
mode = str(data.get("local_proxy", ""))
if mode or group == LOCAL_PROXY_GROUP:
# Режим встроенного движка: адрес у всех один — этот компьютер.
if mode not in MODES or mode in modes or group != LOCAL_PROXY_GROUP:
problems.append(f"{name}: неверный или повторный режим шифрованного DNS «{mode}»")
modes.add(mode)
if list(data.get("ipv4", ())) != ["127.0.0.1"] or list(data.get("ipv6", ())) != ["::1"]:
problems.append(f"{name}: у режима шифрованного DNS адреса должны быть 127.0.0.1 и ::1")
if data.get("doh") or data.get("dot") or data.get("status"):
problems.append(f"{name}: у режима шифрованного DNS не бывает DoH, DoT и пометки состояния")
if not str(data.get("desc", "")).strip() or not str(data.get("icon", "")).strip():
problems.append(f"{name}: нет пояснения или значка")
continue
ipv4 = [str(item) for item in data.get("ipv4", ())]
ipv6 = [str(item) for item in data.get("ipv6", ())]
if not ipv4:
problems.append(f"{name}: нет адреса IPv4")
for version, addresses in ((4, ipv4), (6, ipv6)):
if len(set(addresses)) != len(addresses):
problems.append(f"{name}: адрес IPv{version} записан дважды")
for address in addresses:
try:
valid = ipaddress.ip_address(address).version == version
except ValueError:
valid = False
if not valid:
problems.append(f"{name}: «{address}» — не адрес IPv{version}")
elif owners.setdefault(address, name) != name:
problems.append(f"{name}: адрес {address} уже у сервера {owners[address]}")
doh = str(data.get("doh", ""))
if doh and not doh.startswith("https://"):
problems.append(f"{name}: адрес DoH должен начинаться с https://")
dot = str(data.get("dot", ""))
if dot and ("/" in dot or ":" in dot):
problems.append(f"{name}: в «dot» нужно только имя сервера")
if data.get("encrypted_only") and not doh:
problems.append(f"{name}: сервер только с шифрованием должен иметь адрес DoH")
if data.get("status", "") not in ("", *STATUSES):
problems.append(f"{name}: неизвестная пометка состояния «{data.get('status')}»")
if not str(data.get("desc", "")).strip() or not str(data.get("icon", "")).strip():
problems.append(f"{name}: нет пояснения или значка")
return problems
def doh_templates() -> dict[str, str]:
"""{адрес сервера: шаблон DoH} для всех серверов списка (IPv4 и IPv6).
Windows 11 по этим шаблонам сама шифрует DNS-запросы к известным
серверам, а при недоступности DoH откатывается на обычный DNS.
"""
templates: dict[str, str] = {}
for group in DNS_PROVIDERS.values():
for data in group.values():
template = str(data.get("doh") or "").strip()
if not template:
continue
for address in (*data.get("ipv4", ()), *data.get("ipv6", ())):
templates.setdefault(str(address).strip(), template)
return templates
# Старые адреса DNS и их замены.
# Если пользователь когда-то выбрал этот DNS и адрес остался в настройках
# сетевого адаптера, программа при запуске сама меняет его на новый.
# При каждой смене адресов провайдера добавлять сюда пару «старый → новый».
#
# Замена на сервер только с шифрованием ("encrypted_only") делается лишь там,
# где Windows умеет DoH: иначе человек остался бы без DNS вовсе
# (dns.address_migration.usable_replacements).
OUTDATED_DNS_ADDRESS_REPLACEMENTS = {
# Xbox DNS закрыл сервисы ИИ и отдаёт на них пустой адрес (2026-10-08).
# Все его адреса — действующие, «v2» и старые — программа переводит на
# DNS-AI: он открывает те же сайты.
"111.88.96.54": "192.144.59.14",
"111.88.96.55": "186.246.49.127",
"111.88.96.50": "192.144.59.14",
"111.88.96.51": "186.246.49.127",
"87.228.47.200": "192.144.59.14",
"87.228.47.201": "186.246.49.127",
"176.99.11.77": "192.144.59.14",
"80.78.247.254": "186.246.49.127",
"2a00:ab00:1233:26::50": "2a0d:8480:0:67c::14",
"2a00:ab00:1233:26::51": "2a0a:2b41:0:500d::53",
# Первый адрес Zapret DNS закрыли в России по IP (2026-10-09): сервер переехал.
"144.31.82.230": "83.217.211.149",
"2a01:ecc0:500:8e::2": "2a03:afc0:9::235d",
}