Skip to content

Instantly share code, notes, and snippets.

@odysseus0
Last active June 20, 2025 00:32
Show Gist options
  • Save odysseus0/bd9c3e0cf7f0b2e1222b609577fc4eb1 to your computer and use it in GitHub Desktop.
Save odysseus0/bd9c3e0cf7f0b2e1222b609577fc4eb1 to your computer and use it in GitHub Desktop.
DNS Environment Scanner - Diagnose 'works on my machine' networking issues by comparing DNS and connectivity
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.7"
# ///
"""
DNS Environment Scanner - Diagnose "Works on My Machine" Issues
https://gist.github.com/odysseus0/bd9c3e0cf7f0b2e1222b609577fc4eb1
This tool revealed why httpx AsyncClient worked on my colleague's machine
but not mine: her DNS didn't return IPv6 addresses at all, while mine did.
TikAPI advertises IPv6 but doesn't support it, causing the failure.
Usage: uv run dns_environment_scanner.py [hostname]
"""
import socket
import ssl
import sys
import platform
import subprocess
import json
import time
from typing import Dict, List, Tuple, Optional
def get_system_info() -> Dict[str, str]:
"""Gather system information."""
info = {
'platform': platform.system(),
'platform_version': platform.version(),
'platform_release': platform.release(),
'machine': platform.machine(),
'python_version': sys.version.split()[0],
'python_implementation': platform.python_implementation(),
}
# Get DNS servers on macOS/Linux
if platform.system() in ['Darwin', 'Linux']:
try:
if platform.system() == 'Darwin':
# macOS
result = subprocess.run(['scutil', '--dns'],
capture_output=True, text=True)
nameservers = []
for line in result.stdout.split('\n'):
if 'nameserver' in line and ':' in line:
ns = line.split(':')[1].strip()
if ns and ns not in nameservers:
nameservers.append(ns)
info['dns_servers'] = nameservers[:3] # First 3
else:
# Linux
with open('/etc/resolv.conf', 'r') as f:
nameservers = []
for line in f:
if line.startswith('nameserver'):
ns = line.split()[1]
if ns not in nameservers:
nameservers.append(ns)
info['dns_servers'] = nameservers
except Exception as e:
info['dns_servers'] = [f"Error: {e}"]
return info
def resolve_hostname(hostname: str) -> Dict[str, List[str]]:
"""Resolve a hostname to all its IP addresses."""
results = {
'ipv4': [],
'ipv6': [],
'errors': []
}
try:
# Get all address info
addr_info = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC,
socket.SOCK_STREAM)
for family, _, _, _, sockaddr in addr_info:
ip = sockaddr[0]
if family == socket.AF_INET:
if ip not in results['ipv4']:
results['ipv4'].append(ip)
elif family == socket.AF_INET6:
if ip not in results['ipv6']:
results['ipv6'].append(ip)
except socket.gaierror as e:
results['errors'].append(f"DNS resolution failed: {e}")
except Exception as e:
results['errors'].append(f"Unexpected error: {e}")
return results
def test_connectivity(ip: str, port: int, timeout: int = 5, hostname: str = None) -> Dict[str, any]:
"""Test TCP connectivity and TLS handshake to an IP address."""
result = {
'ip': ip,
'port': port,
'success': False,
'error': None,
'duration': None,
'tls_success': False
}
# Determine socket family
try:
socket.inet_pton(socket.AF_INET, ip)
family = socket.AF_INET
result['family'] = 'IPv4'
except socket.error:
try:
socket.inet_pton(socket.AF_INET6, ip)
family = socket.AF_INET6
result['family'] = 'IPv6'
except socket.error:
result['error'] = "Invalid IP address"
return result
# Test connection
sock = socket.socket(family, socket.SOCK_STREAM)
sock.settimeout(timeout)
start_time = time.time()
try:
# For IPv6, use 4-tuple format
if family == socket.AF_INET6:
sock.connect((ip, port, 0, 0))
else:
sock.connect((ip, port))
result['success'] = True
# Try TLS handshake if hostname provided and HTTPS port
if hostname and port == 443:
try:
import ssl
context = ssl.create_default_context()
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
result['tls_success'] = True
result['tls_version'] = ssock.version()
except Exception as tls_e:
result['tls_error'] = str(tls_e)
result['error'] = f"TCP OK but TLS failed: {tls_e}"
result['success'] = False # Overall failure if TLS fails
result['duration'] = time.time() - start_time
except socket.timeout:
result['error'] = "Connection timeout"
result['duration'] = timeout
except ConnectionRefusedError:
result['error'] = "Connection refused"
result['duration'] = time.time() - start_time
except socket.error as e:
result['error'] = str(e)
result['duration'] = time.time() - start_time
finally:
try:
sock.close()
except:
pass
return result
def diagnose_environment(hostname: str, port: int = 443):
"""Perform complete environment diagnosis."""
print("πŸ” DNS ENVIRONMENT SCANNER")
print("=" * 60)
# System info
print("\nπŸ“± SYSTEM INFORMATION:")
sys_info = get_system_info()
for key, value in sys_info.items():
print(f" {key}: {value}")
# DNS resolution
print(f"\n🌐 DNS RESOLUTION for {hostname}:")
dns_results = resolve_hostname(hostname)
print(f" IPv4 addresses: {dns_results['ipv4'] or ['None']}")
print(f" IPv6 addresses: {dns_results['ipv6'] or ['None']}")
if dns_results['errors']:
print(f" Errors: {dns_results['errors']}")
# Key insight
if dns_results['ipv6'] and not dns_results['ipv4']:
print("\n⚠️ WARNING: Only IPv6 addresses returned!")
elif dns_results['ipv4'] and not dns_results['ipv6']:
print("\nβœ… IPv4-only resolution (this might explain why it works!)")
elif dns_results['ipv4'] and dns_results['ipv6']:
print("\nπŸ”„ Dual-stack: Both IPv4 and IPv6 available")
# Connectivity tests
print(f"\nπŸ§ͺ CONNECTIVITY TESTS (port {port}):")
# Test first IPv4
if dns_results['ipv4']:
print("\nIPv4 Connectivity:")
for ip in dns_results['ipv4'][:2]: # Test first 2
result = test_connectivity(ip, port, hostname=hostname)
status = "βœ…" if result['success'] else "❌"
print(f" {status} {ip}: ", end="")
if result['success']:
tls_info = f" (TLS {result.get('tls_version', 'N/A')})" if result.get('tls_success') else ""
print(f"Connected in {result['duration']:.3f}s{tls_info}")
else:
print(f"{result['error']}")
# Test first IPv6
if dns_results['ipv6']:
print("\nIPv6 Connectivity:")
for ip in dns_results['ipv6'][:2]: # Test first 2
result = test_connectivity(ip, port, hostname=hostname)
status = "βœ…" if result['success'] else "❌"
print(f" {status} {ip}: ", end="")
if result['success']:
tls_info = f" (TLS {result.get('tls_version', 'N/A')})" if result.get('tls_success') else ""
print(f"Connected in {result['duration']:.3f}s{tls_info}")
else:
print(f"{result['error']}")
# Analysis
print("\nπŸ“Š ANALYSIS:")
# Check for IPv6-only failure pattern
ipv4_works = any(test_connectivity(ip, port)['success']
for ip in dns_results['ipv4'][:1])
ipv6_works = any(test_connectivity(ip, port)['success']
for ip in dns_results['ipv6'][:1])
if dns_results['ipv6'] and not ipv6_works and ipv4_works:
print(" 🚨 IPv6 advertised but not working!")
print(" πŸ’‘ This is likely why httpx AsyncClient fails")
print(" (it prefers IPv6 when available)")
if not dns_results['ipv6']:
print(" ℹ️ No IPv6 addresses returned by DNS")
print(" πŸ’‘ This might explain why httpx AsyncClient works here")
print(" (no IPv6 to try, falls back to IPv4)")
# Environment comparison helper
print("\nπŸ“€ SHARE THIS WITH YOUR TEAM:")
share_data = {
'hostname': hostname,
'platform': sys_info['platform'],
'dns_servers': sys_info.get('dns_servers', []),
'ipv4_addresses': dns_results['ipv4'],
'ipv6_addresses': dns_results['ipv6'],
'ipv4_works': ipv4_works,
'ipv6_works': ipv6_works
}
# Add note only if relevant
if dns_results['ipv6'] and not ipv6_works:
share_data['ipv6_note'] = 'TCP works but TLS fails'
print(json.dumps(share_data, indent=2))
print("\nπŸ’‘ To compare environments, have your colleague run:")
print(f" python {sys.argv[0]} {hostname}")
def main():
"""Main entry point."""
if len(sys.argv) < 2:
# Default to testing common problematic services
hostnames = ['api.tikapi.io', 'httpbin.org', 'google.com']
print("Usage: python dns_environment_scanner.py [hostname]")
print(f"\nRunning default test with: {', '.join(hostnames)}")
print()
for hostname in hostnames:
diagnose_environment(hostname)
print("\n" + "=" * 60 + "\n")
else:
hostname = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) > 2 else 443
diagnose_environment(hostname, port)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment