Troubleshoot Hostname Resolution with Python: getaddrinfo, IPv4/IPv6, and Wait Budgets
A background task reports Temporary failure in name resolution. Increasing HTTP retries may only extend the wait. This tutorial builds a separate system hostname-resolution check with the Python standard library: IPv4 and IPv6 results, classified errors, exit codes, and an automation entry point with a wait budget.
It suits beginner-to-intermediate readers and follows the Python TCP port probe. Resolve the name first, then check the port and application response.
1. Method: inspect system resolution
The official socket.getaddrinfo() documentation describes its system-call wrapper. Our tool requests TCP candidates, deduplicates them in returned order, and retains IPv6 scope_id. It makes no connection.
On Linux with glibc, NSS configuration helps determine name sources and lookup order. A result can come from /etc/hosts, a cache, or DNS. Success does not prove that a DNS server was contacted during this run.
Use the Linux getaddrinfo error reference to distinguish:
| status | Meaning and next action |
|---|---|
ok |
At least one address was returned; continue with connection checks. |
temporary_failure |
EAI_AGAIN; investigate the resolution environment before a bounded retry. |
name_or_service_unknown |
EAI_NONAME; check spelling, name sources, and address family. This is not evidence of a DNS NXDOMAIN response. |
resolver_error |
Another resolver error; retain its code and message. |
system_error |
Another system failure; investigate the reported code. |
no_addresses |
No usable addresses; treat as failure. |
2. Setup: no third-party packages
Use Linux and Python 3.9 or newer. Examples were tested on Linux with Python 3.11.4 and change no resolver configuration.
- Create a demonstration directory:
python3 --version
mkdir dns-check-demo
cd dns-check-demo
- Save the complete implementation below as
check_dns.py. Input is an ASCII hostname such aslocalhostorwww.bobobk.com; a trailing dot is allowed. URLs, port suffixes, and IPv6 literals are rejected. For an internationalized domain, use its ASCII/Punycode name first.
3. Implementation: one JSON line and explicit exit codes
"""Inspect system hostname resolution, without connecting. Python 3.9+."""
import argparse
import json
import re
import socket
import time
FAMILIES = {"any": socket.AF_UNSPEC, "ipv4": socket.AF_INET,
"ipv6": socket.AF_INET6}
def hostname(value):
name = value[:-1] if value.endswith(".") else value
label = r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?"
if len(name) > 253 or not re.fullmatch(label + r"(?:\." + label + r")*", name):
raise argparse.ArgumentTypeError("use an ASCII hostname, not a URL")
return value
def resolve(host, port, family):
report = {"host": host, "port": port, "family": family,
"status": "ok", "error_code": None, "error": "", "addresses": []}
started = time.monotonic()
try:
rows = socket.getaddrinfo(host, port, FAMILIES[family],
socket.SOCK_STREAM, socket.IPPROTO_TCP)
seen = set()
for af, _, _, _, address in rows:
if af not in (socket.AF_INET, socket.AF_INET6):
continue
scope = address[3] if af == socket.AF_INET6 else 0
key = (af, address[0], scope)
if key in seen:
continue
seen.add(key)
report["addresses"].append({
"family": "ipv4" if af == socket.AF_INET else "ipv6",
"address": address[0], "scope_id": scope,
})
if not report["addresses"]:
report["status"] = "no_addresses"
except socket.gaierror as exc:
status = {socket.EAI_AGAIN: "temporary_failure",
socket.EAI_NONAME: "name_or_service_unknown"}.get(
exc.errno, "resolver_error")
report.update(status=status, error_code=exc.errno, error=str(exc))
except OSError as exc:
report.update(status="system_error", error_code=exc.errno, error=str(exc))
report["elapsed_ms"] = round((time.monotonic() - started) * 1000, 3)
return report
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("host", type=hostname)
parser.add_argument("--port", type=int, default=443)
parser.add_argument("--family", choices=FAMILIES, default="any")
args = parser.parse_args()
if not 1 <= args.port <= 65535:
parser.error("--port must be between 1 and 65535")
report = resolve(args.host, args.port, args.family)
print(json.dumps(report, ensure_ascii=True))
return 0 if report["status"] == "ok" else 1
if __name__ == "__main__":
raise SystemExit(main())
--port supplies the numeric port for candidate socket addresses; it does not connect to that port. Interpret error_code with status: it can represent an EAI_* code or a system errno. Do not hard-code platform-specific error numbers.
4. Usage: local first, then the failing environment
- Read the help and resolve local IPv4 addresses:
python3 check_dns.py --help
python3 check_dns.py localhost --family ipv4
A typical result follows. Addresses and elapsed time depend on your machine:
{"host":"localhost","port":443,"family":"ipv4","status":"ok","error_code":null,"error":"","addresses":[{"family":"ipv4","address":"127.0.0.1","scope_id":0}],"elapsed_ms":0.2}
- Check the actual hostname inside the failing host or container. This site is an example:
python3 check_dns.py www.bobobk.com
python3 check_dns.py www.bobobk.com --family ipv4
python3 check_dns.py www.bobobk.com --family ipv6
The final command may fail when there is no IPv6 record, IPv6 support is unavailable, or the address family is restricted. That alone does not establish an IPv4 service failure. Records can change; do not hard-code this site’s IP in tests.
Exit 0 means addresses were returned, 1 means the check failed, and 2 means invalid arguments. After resolution succeeds, use curl to check HTTP status and response content.
5. Automation: bound the wait with a child process
getaddrinfo() has no timeout argument exposed by this tool. socket.setdefaulttimeout() is not its resolution timeout control. The observed elapsed_ms is not a deadline.
- Save this as
run_check.pybeside the resolver script:
"""Run check_dns.py in a child process with a five-second wait budget."""
import json
from pathlib import Path
import subprocess
import sys
script = Path(__file__).resolve().with_name("check_dns.py")
try:
result = subprocess.run(
[sys.executable, str(script), *sys.argv[1:]],
capture_output=True, text=True, timeout=5, check=False,
)
except subprocess.TimeoutExpired:
print(json.dumps({"status": "wrapper_timeout", "wait_budget_seconds": 5}))
raise SystemExit(124)
sys.stdout.write(result.stdout)
sys.stderr.write(result.stderr)
raise SystemExit(result.returncode)
The subprocess.run(timeout=...) documentation states that an expired timeout kills and waits for the child. This wrapper keeps resolution in that child, reports wrapper_timeout, and exits 124 when the wait budget expires. This differs from the resolver returning temporary_failure. Process creation and termination can add time, so this is not a guarantee to return within exactly five seconds.
- Run from the same directory. Save the exit code before printing a message to preserve failure:
if python3 run_check.py localhost --family ipv4; then
printf 'Resolution passed; continue with connection checks\n'
else
dns_exit=$?
printf 'Resolution check failed: exit=%s\n' "$dns_exit" >&2
exit "$dns_exit"
fi
For scheduled execution, use absolute paths to the actual interpreter and run_check.py. Follow the cron path and environment checks for the account, working directory, and container environment. A preflight is an observation at that moment; the subsequent client must still handle its own resolution and connection errors.
6. Validation: no public outage required
Save this as test_dns.py. The localhost case uses real system resolution. Injected data covers dual-stack results, duplicates, temporary failures, and other errors, without changing DNS, disconnecting networking, or waiting for a real timeout.
import errno
import json
from pathlib import Path
import socket
import subprocess
import sys
from unittest.mock import patch
import check_dns
root = Path(__file__).resolve().parent
def run(*args):
return subprocess.run([sys.executable, str(root / "check_dns.py"), *args],
capture_output=True, text=True, timeout=5)
# Actual system resolution; no public hostname or remote connection required.
result = run("localhost", "--family", "ipv4")
report = json.loads(result.stdout)
assert result.returncode == 0 and report["status"] == "ok", report
assert report["addresses"] and report["elapsed_ms"] >= 0
assert all(item["family"] == "ipv4" for item in report["addresses"])
rows = [
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.10", 443)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.10", 443)),
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2001:db8::10", 443, 0, 0)),
]
with patch("check_dns.socket.getaddrinfo", return_value=rows) as lookup:
report = check_dns.resolve("example.test", 443, "any")
assert report["status"] == "ok" and len(report["addresses"]) == 2, report
assert [item["family"] for item in report["addresses"]] == ["ipv4", "ipv6"]
lookup.assert_called_once_with("example.test", 443, socket.AF_UNSPEC,
socket.SOCK_STREAM, socket.IPPROTO_TCP)
for failure, expected in [
(socket.gaierror(socket.EAI_AGAIN, "injected temporary failure"), "temporary_failure"),
(socket.gaierror(socket.EAI_NONAME, "injected unknown name"), "name_or_service_unknown"),
(socket.gaierror(socket.EAI_FAIL, "injected failure"), "resolver_error"),
(OSError(errno.EACCES, "injected denial"), "system_error"),
]:
with patch("check_dns.socket.getaddrinfo", side_effect=failure):
report = check_dns.resolve("example.test", 443, "any")
assert report["status"] == expected and report["addresses"] == [], report
assert report["error_code"] == failure.errno, report
with patch("check_dns.socket.getaddrinfo", return_value=[]):
assert check_dns.resolve("example.test", 443, "any")["status"] == "no_addresses"
for arguments in [("https://example.com",), ("bad host",), ("",),
("localhost", "--port", "0"), ("localhost", "--port", "65536"),
("localhost", "--family", "wrong")]:
result = run(*arguments)
assert result.returncode == 2 and not result.stdout and result.stderr, result
print("OK: localhost, dual stack, deduplication, resolver errors, empty result, CLI errors")
python3 test_dns.py
python3 run_check.py localhost --family ipv4
The test script should print:
OK: localhost, dual stack, deduplication, resolver errors, empty result, CLI errors
Injected errors validate classification, not which error a real DNS server will produce. If the local case fails, inspect system resolution for localhost rather than changing the test to ignore failure.
7. Troubleshooting: inspect the caller’s environment
On glibc-based Linux with getent already available, compare these read-only observations:
getent ahosts localhost
getent ahosts www.bobobk.com
cat /etc/nsswitch.conf
cat /etc/resolv.conf
The getent ahosts manual describes its use of getaddrinfo(). Its flags differ from this tool’s, so counts and ordering need not match. Python checks still work when getent is unavailable.
| Observation | Concrete action |
|---|---|
temporary_failure |
Compare resolver configuration and network reachability inside the same container; record the error before deciding on bounded retries. |
name_or_service_unknown |
Check spelling, remove https:// and paths, and check IPv4 and IPv6 separately. |
| Host succeeds, container fails | Compare the container’s own /etc/resolv.conf, NSS, and hosts sources; host results cannot substitute for container results. |
| Resolution succeeds, connection fails | Continue with TCP, TLS, and HTTP checks; an address list does not establish service health. |
wrapper_timeout |
Investigate resolution waits and process state; preserve exit 124 instead of emitting a false success. |
8. Summary
A separate hostname check helps locate failures before or after connection setup. Keep observations in JSON and exit codes, then control automation waits through a child process. Continue with port and response-content checks after resolution succeeds to determine service availability.
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/en/python-dns-getaddrinfo-check.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
相关文章
- Probe TCP Ports with Python: Distinguish Refusal, Timeout, and System Errors
- Test Directory Writes with Python: Permissions, Read-Only Storage, and Path Errors
- Fix Linux Script Startup Errors: Diagnose CRLF and BOM with Python
- Check TLS Certificate Expiry with Python: Verification, Warnings, and Local Tests
- Check ZIP Backups with Python: CRC, Duplicate Members, and Missing Files