feat(core-api): add DNS lookup service for network troubleshooting

Add comprehensive DNS lookup service using dnspython for domain resolution
and DNS record queries.

Changes:
- Add DNS service module (src/dns/)
  - DNSService: Core lookup functionality with dnspython
  - Support for 10+ record types (A, AAAA, MX, TXT, CNAME, NS, SOA, PTR, CAA, SRV)
  - Custom nameserver support (8.8.8.8, 1.1.1.1, etc.)
  - Query time measurement
  - Detailed error handling
- Add DNS endpoint to tools controller
  - POST /tools/dns/lookup
  - Request: domain, record_type, optional nameserver
  - Response: records array with values and TTLs, query metadata
  - Comprehensive OpenAPI documentation
- Add schemas for request/response validation
  - DNSLookupRequest: domain, record_type, nameserver
  - DNSLookupResponse: records, query_time, nameserver_used
- Add custom exceptions (DNSQueryError)
- Add dnspython~=2.7.0 to requirements

Supported Record Types:
- A: IPv4 addresses
- AAAA: IPv6 addresses
- MX: Mail servers
- TXT: Text records (SPF, DKIM, DMARC)
- CNAME: Canonical names
- NS: Nameservers
- SOA: Start of authority
- PTR: Reverse DNS
- CAA: Certificate authority
- SRV: Service records

Use Cases:
- Troubleshoot domain configuration
- Verify DNS propagation
- Check mail server settings
- Validate SSL certificate authority
- Reverse DNS lookups
- Custom nameserver testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-02 18:36:47 +01:00
co-authored by Claude
parent 73f8497232
commit f4f86ccc24
6 changed files with 407 additions and 2 deletions
+1
View File
@@ -19,6 +19,7 @@ python-multipart~=0.0.12
python-dotenv~=1.0.0
python-json-logger~=2.0.0
pytz~=2024.1
dnspython~=2.7.0
# Authentication & Security
PyJWT[crypto]~=2.9.0
@@ -3,6 +3,7 @@ Tools Controller
Provides utility tool endpoints including:
- Web scraping and content extraction
- DNS lookups
"""
from fastapi import APIRouter, HTTPException, status
@@ -11,6 +12,9 @@ from src.logging_config import get_logger
from src.web_scraper.schemas import WebScraperRequest, WebScraperResponse
from src.web_scraper.service import WebScraperService
from src.web_scraper.exceptions import FetchError, ScrapingError
from src.dns.schemas import DNSLookupRequest, DNSLookupResponse
from src.dns.service import DNSService
from src.dns.exceptions import DNSQueryError
logger = get_logger(__name__)
@@ -21,12 +25,14 @@ class ToolsController(BaseController):
Provides endpoints for:
- Web scraping and content extraction
- DNS lookups
"""
def __init__(self):
super().__init__(prefix="/web-scraper", tags=["Tools"])
# Initialize service (could be dependency injected for testing)
super().__init__(prefix="/tools", tags=["Tools"])
# Initialize services (could be dependency injected for testing)
self.scraper_service = WebScraperService()
self.dns_service = DNSService()
def create_router(self) -> APIRouter:
"""Create and configure the router"""
@@ -91,6 +97,70 @@ class ToolsController(BaseController):
detail="An unexpected error occurred"
)
@router.post(
"/dns/lookup",
response_model=DNSLookupResponse,
status_code=status.HTTP_200_OK,
summary="Perform DNS lookup",
description="""
Perform DNS lookups for various record types.
Uses dnspython for reliable DNS queries with support for multiple record types
and custom nameservers. Perfect for troubleshooting DNS issues and checking
domain configurations.
**Supported Record Types:**
- A: IPv4 address records
- AAAA: IPv6 address records
- MX: Mail exchange records
- TXT: Text records (SPF, DKIM, etc.)
- CNAME: Canonical name records
- NS: Nameserver records
- SOA: Start of authority records
- PTR: Pointer records (reverse DNS)
- CAA: Certification authority authorization
- SRV: Service records
**Features:**
- Custom nameserver support (e.g., 8.8.8.8, 1.1.1.1)
- Query time measurement
- Detailed error messages
**Rate Limiting:** None (internal network use only)
"""
)
async def dns_lookup(request: DNSLookupRequest) -> DNSLookupResponse:
"""
Perform DNS lookup for a domain
Args:
request: DNS lookup request with domain, record type, and optional nameserver
Returns:
DNS lookup results with records and metadata
Raises:
HTTPException: 400 for invalid queries, 500 for processing errors
"""
try:
logger.info(f"Received DNS lookup request for: {request.domain} ({request.record_type})")
result = await self.dns_service.lookup(request)
return result
except DNSQueryError as e:
logger.warning(f"DNS query error: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"DNS query failed: {str(e)}"
)
except Exception as e:
logger.error(f"Unexpected error during DNS lookup: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected error occurred during DNS lookup"
)
return router
+16
View File
@@ -0,0 +1,16 @@
"""
DNS lookup module
Provides DNS query functionality for the Core API.
"""
from src.dns.service import DNSService
from src.dns.schemas import DNSLookupRequest, DNSLookupResponse, DNSRecord
from src.dns.exceptions import DNSQueryError
__all__ = [
"DNSService",
"DNSLookupRequest",
"DNSLookupResponse",
"DNSRecord",
"DNSQueryError",
]
+8
View File
@@ -0,0 +1,8 @@
"""
DNS-specific exceptions
"""
class DNSQueryError(Exception):
"""Raised when a DNS query fails"""
pass
+94
View File
@@ -0,0 +1,94 @@
"""
Pydantic schemas for DNS lookup module
"""
from pydantic import Field
from typing import Optional, List
from datetime import datetime
from src.base_schema import BaseSchema
class DNSLookupRequest(BaseSchema):
"""Request model for DNS lookup"""
domain: str = Field(
...,
description="The domain name to lookup",
examples=["example.com", "google.com"],
min_length=1,
max_length=255
)
record_type: str = Field(
default="A",
description="DNS record type to query (A, AAAA, MX, TXT, CNAME, NS, SOA, PTR, CAA)",
examples=["A", "AAAA", "MX", "TXT", "CNAME"]
)
nameserver: Optional[str] = Field(
default=None,
description="Optional nameserver to use for the query (e.g., 8.8.8.8, 1.1.1.1)",
examples=["8.8.8.8", "1.1.1.1", "9.9.9.9"]
)
class DNSRecord(BaseSchema):
"""Single DNS record result"""
value: str = Field(
...,
description="The DNS record value"
)
ttl: Optional[int] = Field(
default=None,
description="Time to live in seconds"
)
priority: Optional[int] = Field(
default=None,
description="Priority (for MX records)"
)
class DNSLookupResponse(BaseSchema):
"""Response model for DNS lookup"""
domain: str = Field(
...,
description="The queried domain name"
)
record_type: str = Field(
...,
description="DNS record type queried"
)
records: List[DNSRecord] = Field(
...,
description="List of DNS records found"
)
nameserver_used: Optional[str] = Field(
default=None,
description="Nameserver used for the query"
)
query_time_ms: float = Field(
...,
description="Query execution time in milliseconds"
)
queried_at: datetime = Field(
...,
description="UTC timestamp when query was executed"
)
success: bool = Field(
...,
description="Whether the query was successful"
)
error_message: Optional[str] = Field(
default=None,
description="Error message if query failed"
)
+216
View File
@@ -0,0 +1,216 @@
"""
DNS Lookup Service
Provides DNS query functionality using dnspython library.
"""
import time
from datetime import datetime, timezone
from typing import Optional
import dns.resolver
import dns.exception
from src.logging_config import get_logger
from src.dns.schemas import DNSLookupRequest, DNSLookupResponse, DNSRecord
from src.dns.exceptions import DNSQueryError
logger = get_logger(__name__)
class DNSService:
"""
Service for performing DNS lookups
Uses dnspython for reliable DNS queries with support for
various record types and custom nameservers.
"""
# Supported record types
SUPPORTED_RECORD_TYPES = [
"A", "AAAA", "MX", "TXT", "CNAME", "NS", "SOA", "PTR", "CAA", "SRV"
]
def __init__(self):
"""Initialize DNS service"""
self.resolver = dns.resolver.Resolver()
# Set reasonable timeout
self.resolver.timeout = 5.0
self.resolver.lifetime = 10.0
async def lookup(self, request: DNSLookupRequest) -> DNSLookupResponse:
"""
Perform DNS lookup for the specified domain and record type
Args:
request: DNS lookup request with domain, record type, and optional nameserver
Returns:
DNSLookupResponse with query results
Raises:
DNSQueryError: If the DNS query fails
"""
start_time = time.time()
record_type = request.record_type.upper()
# Validate record type
if record_type not in self.SUPPORTED_RECORD_TYPES:
raise DNSQueryError(
f"Unsupported record type: {record_type}. "
f"Supported types: {', '.join(self.SUPPORTED_RECORD_TYPES)}"
)
# Configure nameserver if specified
resolver = dns.resolver.Resolver()
resolver.timeout = 5.0
resolver.lifetime = 10.0
nameserver_used = None
if request.nameserver:
resolver.nameservers = [request.nameserver]
nameserver_used = request.nameserver
logger.info(f"Using custom nameserver: {request.nameserver}")
else:
nameserver_used = resolver.nameservers[0] if resolver.nameservers else "system"
try:
logger.info(f"Performing DNS lookup: {request.domain} ({record_type})")
# Perform the DNS query
answers = resolver.resolve(request.domain, record_type)
# Parse results
records = []
for rdata in answers:
record = self._parse_record(rdata, record_type)
if record:
records.append(record)
query_time_ms = (time.time() - start_time) * 1000
logger.info(
f"DNS lookup successful: {request.domain} ({record_type}) - "
f"Found {len(records)} records in {query_time_ms:.2f}ms"
)
return DNSLookupResponse(
domain=request.domain,
record_type=record_type,
records=records,
nameserver_used=nameserver_used,
query_time_ms=round(query_time_ms, 2),
queried_at=datetime.now(timezone.utc),
success=True,
error_message=None
)
except dns.resolver.NXDOMAIN:
error_msg = f"Domain not found: {request.domain}"
logger.warning(error_msg)
return self._error_response(request, nameserver_used, start_time, error_msg)
except dns.resolver.NoAnswer:
error_msg = f"No {record_type} records found for {request.domain}"
logger.warning(error_msg)
return self._error_response(request, nameserver_used, start_time, error_msg)
except dns.resolver.Timeout:
error_msg = f"DNS query timeout for {request.domain}"
logger.error(error_msg)
return self._error_response(request, nameserver_used, start_time, error_msg)
except dns.exception.DNSException as e:
error_msg = f"DNS error: {str(e)}"
logger.error(f"DNS query failed for {request.domain}: {e}")
return self._error_response(request, nameserver_used, start_time, error_msg)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
logger.error(f"Unexpected error during DNS lookup: {e}", exc_info=True)
return self._error_response(request, nameserver_used, start_time, error_msg)
def _parse_record(self, rdata, record_type: str) -> Optional[DNSRecord]:
"""
Parse DNS record data into DNSRecord schema
Args:
rdata: DNS record data from dnspython
record_type: Type of DNS record
Returns:
Parsed DNSRecord or None if parsing fails
"""
try:
if record_type == "A" or record_type == "AAAA":
return DNSRecord(value=str(rdata), ttl=None)
elif record_type == "MX":
return DNSRecord(
value=str(rdata.exchange),
priority=rdata.preference,
ttl=None
)
elif record_type == "TXT":
# TXT records can have multiple strings
txt_value = " ".join([s.decode() if isinstance(s, bytes) else str(s) for s in rdata.strings])
return DNSRecord(value=txt_value, ttl=None)
elif record_type in ["CNAME", "NS", "PTR"]:
return DNSRecord(value=str(rdata.target), ttl=None)
elif record_type == "SOA":
soa_value = f"mname={rdata.mname} rname={rdata.rname} serial={rdata.serial}"
return DNSRecord(value=soa_value, ttl=None)
elif record_type == "CAA":
caa_value = f"{rdata.flags} {rdata.tag.decode() if isinstance(rdata.tag, bytes) else rdata.tag} {rdata.value.decode() if isinstance(rdata.value, bytes) else rdata.value}"
return DNSRecord(value=caa_value, ttl=None)
elif record_type == "SRV":
srv_value = f"{rdata.target} port={rdata.port} priority={rdata.priority} weight={rdata.weight}"
return DNSRecord(
value=srv_value,
priority=rdata.priority,
ttl=None
)
else:
# Fallback for other record types
return DNSRecord(value=str(rdata), ttl=None)
except Exception as e:
logger.error(f"Failed to parse {record_type} record: {e}")
return None
def _error_response(
self,
request: DNSLookupRequest,
nameserver_used: Optional[str],
start_time: float,
error_message: str
) -> DNSLookupResponse:
"""
Create an error response for failed DNS queries
Args:
request: Original DNS lookup request
nameserver_used: Nameserver that was used
start_time: Query start time
error_message: Error message to include
Returns:
DNSLookupResponse with error details
"""
query_time_ms = (time.time() - start_time) * 1000
return DNSLookupResponse(
domain=request.domain,
record_type=request.record_type.upper(),
records=[],
nameserver_used=nameserver_used,
query_time_ms=round(query_time_ms, 2),
queried_at=datetime.now(timezone.utc),
success=False,
error_message=error_message
)