1313from typing import AsyncIterator , Dict , List , Optional , Tuple
1414
1515try :
16- import aiodns
17- AIODNS_AVAILABLE = True
16+ import dns .asyncresolver
17+ import dns .exception
18+ import dns .resolver
19+ DNSPYTHON_AVAILABLE = True
1820except ImportError :
19- AIODNS_AVAILABLE = False
21+ DNSPYTHON_AVAILABLE = False
2022
2123# Configuration
2224TRUSTED_RESOLVERS = ["1.1.1.1" , "8.8.8.8" ]
@@ -59,9 +61,9 @@ def __init__(
5961 batch_size : int = BATCH_SIZE ,
6062 verbose : bool = False
6163 ) -> None :
62- if not AIODNS_AVAILABLE :
64+ if not DNSPYTHON_AVAILABLE :
6365 raise ImportError (
64- "aiodns required for Validator. Install with: pip install aiodns \n "
66+ "dnspython required for Validator. Install with: pip install dnspython \n "
6567 "Or: pip install -r requirements.txt"
6668 )
6769
@@ -77,9 +79,6 @@ def __init__(
7779 self .verbose = verbose
7880 self ._baseline_ip = ""
7981 self ._baseline_data : Dict [str , Dict ] = {}
80- # Resolver cache to prevent creating too many instances
81- self ._resolver_cache : Dict [str , aiodns .DNSResolver ] = {}
82- self ._cache_lock : Optional [asyncio .Lock ] = None
8382
8483 @staticmethod
8584 def _random_subdomain (length : int = SUBDOMAIN_LENGTH ) -> str :
@@ -99,51 +98,42 @@ def _log(self, msg: str) -> None:
9998 if self .verbose :
10099 print (msg )
101100
102- async def _get_resolver (self , nameserver : str , timeout : Optional [int ] = None ) -> aiodns . DNSResolver :
103- """Get or create a cached DNS resolver for the given nameserver .
101+ def _create_resolver (self , nameserver : str , timeout : Optional [float ] = None ) -> dns . asyncresolver . Resolver :
102+ """Create a DNS resolver using dnspython (no inotify watches) .
104103
105- This prevents creating too many resolver instances which would exhaust
106- inotify watches on Linux systems.
104+ dnspython doesn't use c-ares, so it avoids inotify watch exhaustion.
107105 """
108- # Initialize lock on first use
109- if self ._cache_lock is None :
110- self ._cache_lock = asyncio .Lock ()
111-
112- cache_key = f"{ nameserver } :{ timeout or self .timeout } "
113-
114- async with self ._cache_lock :
115- if cache_key not in self ._resolver_cache :
116- self ._resolver_cache [cache_key ] = aiodns .DNSResolver (
117- nameservers = [nameserver ],
118- timeout = timeout or self .timeout
119- )
120- return self ._resolver_cache [cache_key ]
106+ resolver = dns .asyncresolver .Resolver ()
107+ resolver .nameservers = [nameserver ]
108+ resolver .timeout = timeout or self .timeout
109+ resolver .lifetime = timeout or self .timeout
110+ return resolver
121111
122112 async def _setup_baseline_single (self , resolver_ip : str ) -> bool :
123113 """Setup baseline from single trusted resolver."""
124114 self ._log (f"[INFO] { resolver_ip } - Establishing baseline" )
125115 try :
126- resolver = await self ._get_resolver (resolver_ip )
116+ resolver = self ._create_resolver (resolver_ip )
127117 data = {}
128118
129119 # Get baseline IP
130- result = await resolver .query (self .baseline_domain , 'A' )
131- data ["ip" ] = self ._baseline_ip = result [0 ]. host
120+ result = await resolver .resolve (self .baseline_domain , 'A' )
121+ data ["ip" ] = self ._baseline_ip = str ( result [0 ])
132122
133123 # Test domains in parallel
134- domain_tasks = [resolver .query (domain , 'A' ) for domain in self .test_domains ]
124+ domain_tasks = [resolver .resolve (domain , 'A' ) for domain in self .test_domains ]
135125 domain_results = await asyncio .gather (* domain_tasks , return_exceptions = True )
136126
137127 data ["domains" ] = {}
138128 for domain , result in zip (self .test_domains , domain_results ):
139129 if not isinstance (result , Exception ):
140- data ["domains" ][domain ] = result [0 ]. host
130+ data ["domains" ][domain ] = str ( result [0 ])
141131
142132 # NXDOMAIN check
143133 try :
144- await resolver .query (self .query_prefix + self .baseline_domain , 'A' )
134+ await resolver .resolve (self .query_prefix + self .baseline_domain , 'A' )
145135 data ["nxdomain" ] = False
146- except aiodns . error . DNSError :
136+ except ( dns . exception . DNSException , dns . resolver . NXDOMAIN ) :
147137 data ["nxdomain" ] = True
148138
149139 self ._baseline_data [resolver_ip ] = data
@@ -159,14 +149,14 @@ async def _setup_baseline(self) -> bool:
159149 success_count = sum (1 for r in results if r is True )
160150 return success_count == len (self .trusted_resolvers )
161151
162- async def _check_poisoning (self , resolver : aiodns . DNSResolver , server : str ) -> Optional [str ]:
152+ async def _check_poisoning (self , resolver : dns . asyncresolver . Resolver , server : str ) -> Optional [str ]:
163153 """Check for DNS poisoning with parallel queries.
164154
165- All 5 poison domains are checked in parallel for maximum speed.
155+ All poison domains are checked in parallel for maximum speed.
166156 Returns immediately if ANY domain resolves (indicating poisoning).
167157 """
168158 subdomains = [f"{ self ._random_subdomain ()} .{ domain } " for domain in self .poison_check_domains ]
169- tasks = [resolver .query (subdomain , 'A' ) for subdomain in subdomains ]
159+ tasks = [resolver .resolve (subdomain , 'A' ) for subdomain in subdomains ]
170160 results = await asyncio .gather (* tasks , return_exceptions = True )
171161
172162 for subdomain , result in zip (subdomains , results ):
@@ -176,15 +166,15 @@ async def _check_poisoning(self, resolver: aiodns.DNSResolver, server: str) -> O
176166 return None
177167
178168 async def _check_nxdomain_and_baseline (
179- self , resolver : aiodns . DNSResolver , server : str
169+ self , resolver : dns . asyncresolver . Resolver , server : str
180170 ) -> Tuple [bool , bool , Optional [str ]]:
181171 """Combined NXDOMAIN and baseline validation check. Returns (has_nxdomain, baseline_matches, error)."""
182172 subdomain = f"{ self ._random_subdomain ()} .{ self .baseline_domain } "
183173
184174 try :
185175 # Check NXDOMAIN and baseline in parallel
186- nxdomain_task = resolver .query (subdomain , 'A' )
187- baseline_task = resolver .query (self .baseline_domain , 'A' )
176+ nxdomain_task = resolver .resolve (subdomain , 'A' )
177+ baseline_task = resolver .resolve (self .baseline_domain , 'A' )
188178
189179 nxdomain_result , baseline_result = await asyncio .gather (
190180 nxdomain_task , baseline_task , return_exceptions = True
@@ -196,7 +186,7 @@ async def _check_nxdomain_and_baseline(
196186 # Baseline should match
197187 baseline_matches = False
198188 if not isinstance (baseline_result , Exception ):
199- resolved_ip = baseline_result [0 ]. host
189+ resolved_ip = str ( baseline_result [0 ])
200190 baseline_matches = resolved_ip == self ._baseline_ip
201191
202192 return has_nxdomain , baseline_matches , None
@@ -215,9 +205,9 @@ def _matches_baseline(self, has_nxdomain: bool) -> bool:
215205 async def _measure_latency (self , server : str ) -> float :
216206 """Measure simple DNS query latency."""
217207 try :
218- resolver = await self ._get_resolver (server , timeout = 1 )
208+ resolver = self ._create_resolver (server , timeout = 1 )
219209 start = time .time ()
220- await resolver .query (self .baseline_domain , 'A' )
210+ await resolver .resolve (self .baseline_domain , 'A' )
221211 return (time .time () - start ) * 1000
222212 except :
223213 return - 1
@@ -229,7 +219,7 @@ async def _validate_server(self, server: str) -> ValidationResult:
229219
230220 try :
231221 timeout = FAST_TIMEOUT if self .use_fast_timeout else self .timeout
232- resolver = await self ._get_resolver (server , timeout = timeout )
222+ resolver = self ._create_resolver (server , timeout = timeout )
233223
234224 # Run ALL checks in parallel for max speed
235225 poison_task = self ._check_poisoning (resolver , server )
0 commit comments