zapretgui/tests/test_dns_winapi.py
loop-uh 28292aba7c
All checks were successful
Zapret source guards / validate (push) Successful in 6s
Windows-сборка / Dev Dev 21.1.6.36 опубликован
DNS переписан на чистом WinAPI: только настоящие адаптеры, виртуалки и VPN не трогаются; добавлены AstraCat и GeoHide
- dns/winapi.py: GetAdaptersAddresses без GAA_FLAG_INCLUDE_ALL_INTERFACES
  (из-за него на странице были WFP/QoS/Npcap-фильтры, отладчик ядра и т.п.),
  признаки «аппаратный / фильтр / разъём» из GetIfEntry2, адаптер интернета
  через GetBestInterfaceEx, DNS читается GetInterfaceDnsSettings вместо
  реестра, пишется SetInterfaceDnsSettings (DoH на Windows 11), кэш —
  DnsFlushResolverCache.
- dns/adapters.py: показываются настоящие сетевые карты (в том числе внутри
  виртуалки) и адаптер, через который идёт интернет (Hyper-V vEthernet);
  внутренние сети Hyper-V/VMware/VirtualBox/WSL, VPN и Wi-Fi Direct не
  показываются и не меняются.
- Адаптер опознаётся по GUID; запись только адаптерам со страницы — Windows
  молча заводит ветку реестра под любой GUID.
- Страница: таблетки со значком Ethernet/Wi-Fi и пометками «интернет» /
  «не подключён», по умолчанию отмечены только подключённые, отметки
  сохраняются после применения; ошибки Windows видны во всплывашке.
- Шаблоны DoH берутся из списка серверов (один источник), IPv6 тоже с DoH.
- Новые DNS из профилей hosts: AstraCat и GeoHide (проверены с Windows).
- Удалены мёртвые dns_core, dns_force, dns_worker, «DNS при запуске»,
  ключи force_dns_enabled и dns_crash_count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:43:52 +03:00

163 lines
7.3 KiB
Python

from __future__ import annotations
import ctypes
import sys
import unittest
from unittest.mock import patch
from dns import winapi
class _FakeIpHelper:
"""Записывает, что именно ушло в SetInterfaceDnsSettings."""
def __init__(self, result: int = 0) -> None:
self.result = result
self.calls: list[dict] = []
def SetInterfaceDnsSettings(self, guid, settings_ptr): # noqa: N802
base = ctypes.cast(settings_ptr, ctypes.POINTER(winapi.DNS_INTERFACE_SETTINGS)).contents
call = {"guid": winapi.guid_to_string(guid), "version": base.Version, "flags": base.Flags, "name_server": base.NameServer}
if base.Version == winapi.DNS_INTERFACE_SETTINGS_VERSION3:
full = ctypes.cast(settings_ptr, ctypes.POINTER(winapi.DNS_INTERFACE_SETTINGS3)).contents
call["properties"] = [
(
full.ServerProperties[index].ServerIndex,
full.ServerProperties[index].Type,
full.ServerProperties[index].Property.DohSettings.contents.Template,
full.ServerProperties[index].Property.DohSettings.contents.Flags,
)
for index in range(full.cServerProperties)
]
self.calls.append(call)
return self.result
GUID_TEXT = "{553DA581-A1E2-4F9E-BDEF-E3DC2C1CE39F}"
class DnsWinApiTests(unittest.TestCase):
def test_structures_match_windows_sdk_sizes(self) -> None:
# Проверено на Windows x64: sizeof(MIB_IF_ROW2) == 1352 (wchar_t там 2 байта).
if sys.platform == "win32" and ctypes.sizeof(ctypes.c_void_p) == 8:
self.assertEqual(ctypes.sizeof(winapi.MIB_IF_ROW2), 1352)
self.assertEqual(ctypes.sizeof(winapi.GUID), 16)
def test_guid_round_trip(self) -> None:
guid = winapi.guid_from_string(GUID_TEXT)
self.assertEqual(winapi.guid_to_string(guid), GUID_TEXT)
self.assertEqual(winapi.guid_to_string(winapi.guid_from_string(GUID_TEXT.strip("{}").lower())), GUID_TEXT)
with self.assertRaises(ValueError):
winapi.guid_from_string("Ethernet")
def test_name_servers_are_split_without_duplicates(self) -> None:
self.assertEqual(winapi.split_name_servers("1.1.1.1,1.0.0.1; 1.1.1.1"), ("1.1.1.1", "1.0.0.1"))
self.assertEqual(winapi.split_name_servers("2606:4700::1111 2606:4700::1001"), ("2606:4700::1111", "2606:4700::1001"))
self.assertEqual(winapi.split_name_servers(None), ())
def test_plain_write_uses_version1_settings(self) -> None:
fake = _FakeIpHelper()
with patch.object(winapi, "_iphlpapi", return_value=fake):
winapi.write_dns(GUID_TEXT, ["8.8.8.8", "8.8.4.4", "8.8.8.8"], ipv6=False)
call = fake.calls[0]
self.assertEqual(call["guid"], GUID_TEXT)
self.assertEqual(call["version"], winapi.DNS_INTERFACE_SETTINGS_VERSION1)
self.assertEqual(call["flags"], winapi.DNS_SETTING_NAMESERVER)
self.assertEqual(call["name_server"], "8.8.8.8,8.8.4.4")
def test_doh_write_attaches_template_to_known_servers(self) -> None:
fake = _FakeIpHelper()
with patch.object(winapi, "_iphlpapi", return_value=fake):
winapi.write_dns(
GUID_TEXT,
["2606:4700:4700::1111", "2001:db8::1"],
ipv6=True,
doh_templates={"2606:4700:4700::1111": "https://cloudflare-dns.com/dns-query"},
)
call = fake.calls[0]
self.assertEqual(call["version"], winapi.DNS_INTERFACE_SETTINGS_VERSION3)
self.assertEqual(call["flags"], winapi.DNS_SETTING_NAMESERVER | winapi.DNS_SETTING_IPV6 | winapi.DNS_SETTING_DOH)
self.assertEqual(call["name_server"], "2606:4700:4700::1111 2001:db8::1")
self.assertEqual(
call["properties"],
[
(
0,
winapi.DNS_SERVER_DOH_PROPERTY,
"https://cloudflare-dns.com/dns-query",
winapi.DNS_DOH_SERVER_SETTINGS_ENABLE | winapi.DNS_DOH_SERVER_SETTINGS_FALLBACK_TO_UDP,
)
],
)
def test_empty_write_returns_to_automatic_and_errors_are_readable(self) -> None:
fake = _FakeIpHelper(result=5)
with patch.object(winapi, "_iphlpapi", return_value=fake):
with self.assertRaises(winapi.DnsWinApiError) as error:
winapi.write_dns(GUID_TEXT, [], ipv6=False, doh_templates={})
self.assertEqual(fake.calls[0]["name_server"], "")
self.assertIn("SetInterfaceDnsSettings: ошибка Windows 5", str(error.exception))
def test_interface_chain_skips_loopback_and_reads_dns_servers(self) -> None:
def sockaddr_v4(address: str):
raw = (ctypes.c_ubyte * 16)(2, 0, 0, 0, *[int(part) for part in address.split(".")])
return raw
keep: list[object] = []
def dns_node(address: str, next_node=None):
raw = sockaddr_v4(address)
node = winapi.IP_ADAPTER_DNS_SERVER_ADDRESS()
node.Address.lpSockaddr = ctypes.cast(raw, ctypes.c_void_p)
node.Address.iSockaddrLength = 16
if next_node is not None:
node.Next = ctypes.pointer(next_node)
keep.extend([raw, node])
return node
def adapter(index, if_type, name, guid, dns=None, next_node=None):
item = winapi.IP_ADAPTER_ADDRESSES()
item.IfIndex = index
item.IfType = if_type
item.OperStatus = winapi.IF_OPER_STATUS_UP
item.FriendlyName = name
item.Description = name + " desc"
item.AdapterName = guid.encode("ascii")
if dns is not None:
item.FirstDnsServerAddress = ctypes.pointer(dns)
if next_node is not None:
item.Next = ctypes.pointer(next_node)
keep.append(item)
return item
ethernet = adapter(9, winapi.IF_TYPE_ETHERNET_CSMACD, "Ethernet", GUID_TEXT, dns=dns_node("1.1.1.1", dns_node("8.8.8.8")))
loopback = adapter(1, winapi.IF_TYPE_SOFTWARE_LOOPBACK, "Loopback", "{00000000-0000-0000-0000-000000000001}", next_node=ethernet)
with patch.object(winapi, "_interface_flags", return_value=winapi.IF_FLAG_HARDWARE | winapi.IF_FLAG_CONNECTOR_PRESENT):
interfaces = winapi._read_interfaces(ctypes.pointer(loopback))
self.assertEqual(len(interfaces), 1)
self.assertEqual(interfaces[0].name, "Ethernet")
self.assertEqual(interfaces[0].guid, GUID_TEXT)
self.assertEqual(interfaces[0].dns_ipv4, ("1.1.1.1", "8.8.8.8"))
self.assertTrue(interfaces[0].hardware and interfaces[0].connector and interfaces[0].connected)
def test_hidden_interfaces_are_not_requested(self) -> None:
source = open(winapi.__file__, encoding="utf-8").read()
self.assertNotIn("GAA_FLAG_INCLUDE_ALL_INTERFACES", source.split('"""', 2)[2])
self.assertNotIn("winreg", source)
def test_doh_needs_windows_11(self) -> None:
with patch.object(winapi, "windows_build", return_value=19045):
self.assertFalse(winapi.is_doh_supported())
with patch.object(winapi, "windows_build", return_value=22631):
self.assertTrue(winapi.is_doh_supported())
if __name__ == "__main__":
unittest.main()