ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DNS解析成功,网页为什么还是打不开?用四个真实结果拆开DNS、TCP和HTTP

DNS解析成功,网页为什么还是打不开?用四个真实结果拆开DNS、TCP和HTTP DNS解析成功网页为什么还是打不开用四个真实结果拆开DNS、TCP和HTTP遇到网页打不开常见建议是换 DNS。但如果 DNS 已经给出了地址问题还一定在 DNS 吗反过来一个网站返回 403能不能称为“网络不通”我们用自建的隔离服务做四个对照。域名和 IP 都一样改变端口或请求路径观察不同层次的结果。1. 先把“打开网页”拆成几个问题这篇用明文 HTTP/1.x over TCP把路径缩小到最容易观察的几个阶段域名解析拿到哪个地址 TCP连接能否连接这个地址的指定端口 HTTP响应对方怎样回答这次请求 内容检查回答是否符合我的预期这不是所有现代网页的完整流程图。HTTPS还有TLSHTTP/3使用QUIC浏览器还可能使用代理、缓存或不同地址族。这里先把这些变量排除不把一个小实验冒充完整浏览器诊断。每一步回答不同的问题上一层成功不保证下一层成功。2. 一张只属于实验的网络WSL 的 Ubuntu-24.04 中两个新建的网络命名空间通过 veth 连接节点地址服务客户端C192.0.2.20/24查询与请求程序服务端S192.0.2.1/24dnsmasq本地DNS、Python HTTP服务服务端把service.test的 A 记录设为192.0.2.1。HTTP只监听80808081没有监听程序。它没有互联网或家庭网络上联。HTTP服务使用 Python 3.12.3 的标准库HTTPServer与BaseHTTPRequestHandler只定义几个固定响应没有自己实现HTTP解析器也没有对外共享文件目录。官方说明该模块不适合生产部署这里只用于隔离教学实验。Python http.server文档3. DNS成功精确地证明了什么在客户端命名空间内指定本地DNS查询nslookup -typeA service.test 192.0.2.1 Name: service.test Address: 192.0.2.1这是本次工具真实输出中的关键行。验证程序从这个答案提取地址再用于后续连接不是无视查询结果另写一个目标。它证明这次向指定DNS发起的A查询得到了预期地址。它不证明该IP的8080或8081端口能连接。HTTP服务接受某条路径。HTTPS证书和TLS握手正常。浏览器使用的也是这个DNS、这条答案或这个地址族。DNS答案不包含普通网页URL里的路径这次8080/8081也由请求配置指定不能从相同A记录推出所有端口行为相同。4. 四个请求四种不同结果实际结果如下请求DNS A答案TCPHTTP状态响应体/预期端口8081192.0.2.1连接被拒绝没有HTTP响应未进入HTTP阶段8080的/同上成功200lab-ok符合约定8080的/private同上成功403lab-denied实验主动拒绝8080的/wrong-result同上成功200lab-error不符合约定连接被拒绝不要继续分析网页内容访问8081时Python得到ConnectionRefusedError。这个隔离服务端没有监听8081因此这次没有拿到HTTP响应。注意“拒绝”和“超时”不是一个结果。拒绝是明确的连接错误超时则是截止时间内没有完成预期操作真实环境中拒绝也可能来自中间设备的主动拒绝策略不能看见这个异常就断定目标机器一定关了服务。本次没有抓TCP包因此不展示虚构的RST截图只记录套接字实际返回的错误。HTTP403连接已经走到了HTTP响应阶段访问/private时TCP连接成功对端为192.0.2.1:8080随后收到了403和固定响应体。403表示服务端理解请求但拒绝执行。HTTP语义 RFC 9110在本实验里拒绝由我们定义的处理器直接生成。但真实网站的403也可能由网关、代理、CDN等组件生成所以“收到403”不能单独定位到最终业务服务更不能直接归因于某一种账号或权限问题。这时继续反复换DNS通常不是由当前证据推出的修复动作。先看谁响应、哪个URL、请求条件和响应内容。HTTP200还要对照应用约定为了暴露另一种误判实验服务的/wrong-result故意返回状态200却把正文设为lab-error。客户端约定正常结果必须是lab-ok所以它能区分“HTTP响应已收到”和“内容检查通过”。这不是在建议服务端用200表示错误也不是说200没有意义它说明应用需要明确的成功契约不能只判断状态码就忽略响应体。200状态语义真实接口可能把业务结果放在JSON字段里网页也可能返回一个200的错误提示页。必须按具体应用协议判断不把实验中的字符串约定泛化成HTTP标准。5. 代码中怎样把这几步分开测试端使用标准库先显式建立TCP连接再发送HTTP请求并读取正文。下面是核心结构address来自前面的DNS答案path来自实验用例connection http.client.HTTPConnection(address, 8080, timeout2) try: connection.connect() peer connection.sock.getpeername() connection.request(GET, path, headers{Host: service.test:8080}) response connection.getresponse() body response.read().decode(ascii) finally: connection.close()这不是完整程序完整脚本还定义了地址、用例和断言。代码刻意连接DNS答案对应的IP同时保留HTTPHost为测试域名。这样无需修改宿主的hosts文件或系统解析器也不丢掉HTTP请求中的主机名语义。不过它测试的是“显式DNS查询再按该答案发请求”不是操作系统自动解析整条URL。没有TLS自然也没有测试HTTPS的SNI或证书名称校验不能把这个方法直接当成HTTPS访问的等价替代。6. 验证程序怎样避免混淆失败本次验证没有把所有异常都当作预期失败8081必须明确得到连接拒绝其他异常会使实验失败。8080的三个请求必须真正连上记录对端并验证状态和正文。200/403和正文分别断言不用一个模糊的“请求成功”覆盖全部。HTTP服务在绑定监听后写就绪标记客户端看到标记才发送避免启动竞态。四个用例都得到预期结果最终passedtrue。这个字段的含义是“实验观察符合预设断言”不是“所有页面都能正常使用”。服务进程和命名空间在退出时清理没有公网发布、没有扫描第三方端口也没有修改正在运行的家庭路由器。完整源码、依赖准备和运行命令放在文末附录。正文中的命名空间变量由编排脚本创建不需要在真实网卡上手动制造故障。7. 实际排障把“打不开”换成能验证的问题面对网站故障我会先整理这些信息阶段记录什么不要急着下的结论解析查询哪个域名、哪个DNS、哪种记录、答案是什么任意浏览器都用了这个答案选路与连接实际目标IP/端口、出接口、错误类型ping成功就代表TCP端口可用HTTP/TLS协议、状态码或握手错误、响应来自哪里403一定来自最终业务程序内容与业务正文、业务字段、预期契约200就代表所有功能正常不要把本实验的HTTP-over-TCP步骤套到所有协议也不要仅凭IP可达就省掉证书、Host、代理和应用层检查。为什么一次只改一个条件本例的两个控制变量很有用8080和8081的DNS答案相同只改变端口就能观察连接阶段的差异/与/private使用相同IP和端口只改变路径就能观察HTTP处理的差异。若同时更换DNS、切换网络、重启服务再说“换DNS修好了”就不知道究竟是哪一步起了作用。在自己的设备或有权排查的服务上可以先做一份很小的记录再决定改什么已拿到的证据下一步优先核对先不要做什么指定DNS返回了地址但连接被拒绝实际IP/端口是否正确自己的服务是否监听是否有主动拒绝规则凭这一结果反复换DNS已收到403响应谁返回响应、路径和请求条件自己服务的访问日志与规则把HTTP拒绝说成DNS没解析成功收到200但内容不对响应体和应用成功契约是否返回了错误页或非预期内容仅以200宣布问题解决命令行正常浏览器仍失败两者是否用了相同地址、协议、端口及网络路径用一次命令行结果代替浏览器证据最后一行只是待核对的问题不是这次实验已经证明的浏览器故障原因。排障的价值不在于列出更多命令而在于让每一步结果决定下一步检查并在修复后重新验证原来的失败条件。DNS成功是下一步检查的起点不是网络排障的终点。同一个名字、同一个IP仍能在不同端口、路径和应用约定下产生完全不同的结果。附录完整源码与复现以下是完整文件不需要先下载仓库。环境限定为 Ubuntu 24.04 amd64本次为 WSL2需要 Bash、iproute2、ping、Python 3、sysctl、timeout 和 dpkg-deb不能把 Windows PowerShell 当成 Bash。实验需要 root 和创建网络命名空间的权限普通受限容器可能无法运行。不要在生产路由器上运行。在 Linux 中建立任意工作目录例如mkdir -p ~/network-lab-dns-tcp-http/scripts cd ~/network-lab-dns-tcp-http将下面每个代码块按标题文件名放入 scripts 目录使用 UTF-8 和 LF 换行。脚本按自身位置寻找文件不依赖我的 D 盘路径。先执行bash scripts/prepare-network-lab-tools.sh仅下载并解包软件包不安装系统服务。该辅助程序准备这组网络实验共用的工具部分工具本篇不会调用。需要正常的软件源、网络连接和包索引索引过期时先自行更新索引。在线下载分支尚未在本轮重新测试本轮使用已有官方 deb 缓存验证解包和运行。文件scripts/prepare-network-lab-tools.sh#!/usr/bin/env bash set -euo pipefail base$(cd -- $(dirname -- $0)/.. pwd) tools$base/tmp/dhcp-tools-20261004 . /etc/os-release test $ID ubuntu test $VERSION_ID 24.04 test $(dpkg --print-architecture) amd64 for tool in ip ping python3 sysctl dpkg-deb timeout; do command -v $tool /dev/null; done packages(dnsmasq-base busybox-static libnetfilter-conntrack3 libnftables1 libnfnetlink0 libmnl0 libnftnl11 libxtables12 nftables conntrack) mkdir -p $tools stage$(mktemp -d $tools/download.XXXXXX) if test $# 2 test $1 --from-cache; then test -d $2 for deb in $2/*.deb; do cp -- $deb $stage/; done elif test $# 0; then (cd $stage apt-get download ${packages[]}) else echo Usage: prepare-network-lab-tools.sh [--from-cache PACKAGE_DIRECTORY] 2 exit 1 fi for package in ${packages[]}; do found0 for deb in $stage/*.deb; do if test $(dpkg-deb -f $deb Package) $package; then found1; break; fi done test $found 1 || { echo Missing package: $package 2; exit 1; } done : $tools/package-versions.txt for deb in $stage/*.deb; do dpkg-deb -x $deb $tools/extracted dpkg-deb -f $deb Package Version Architecture $tools/package-versions.txt cp -- $deb $tools/ done export LD_LIBRARY_PATH$tools/extracted/usr/lib/x86_64-linux-gnu:$tools/extracted/lib/x86_64-linux-gnu $tools/extracted/usr/sbin/dnsmasq --version | head -n 2 $tools/extracted/usr/sbin/nft --version $tools/extracted/usr/sbin/conntrack --version echo Tools ready. No package installation or service startup was performed.文件scripts/verify-network-http-layers.sh#!/usr/bin/env bash set -euo pipefail test $(id -u) 0 base$(cd -- $(dirname -- $0)/.. pwd) tools$base/tmp/dhcp-tools-20261004/extracted busy$tools/usr/bin/busybox dnsmasq$tools/usr/sbin/dnsmasq export LD_LIBRARY_PATH$tools/usr/lib/x86_64-linux-gnu:$tools/lib/x86_64-linux-gnu test -x $busy test -x $dnsmasq out$base/data/verification/network-http-layers-$(date %Y%m%d-%H%M%S) mkdir -p $out exec (tee $out/stdout.txt) 21 python3 --version token$(cat /proc/sys/kernel/random/uuid) cht-${token:0:8}-c sht-${token:0:8}-s created() pids() cleanup() { for pid in ${pids[]}; do kill $pid 2/dev/null || true; wait $pid 2/dev/null || true; done for ns in ${created[]}; do ip netns del $ns; done } trap cleanup EXIT for ns in $c $s; do ip netns add $ns; created($ns); ip -n $ns link set lo up; done ip -n $c link add eth0 type veth peer name eth0 netns $s ip -n $c link set eth0 up ip -n $s link set eth0 up ip -n $c addr add 192.0.2.20/24 dev eth0 ip -n $s addr add 192.0.2.1/24 dev eth0 ip netns exec $s $dnsmasq --keep-in-foreground --conf-file/dev/null \ --interfaceeth0 --bind-interfaces --no-resolv --no-hosts --userroot \ --address/service.test/192.0.2.1 --pid-file$out/dns.pid $out/dns-server.txt 21 pids($!) ip netns exec $s python3 $base/scripts/lab-http-layers.py serve $out $out/http-server.txt 21 pids($!) for attempt in $(seq 1 100); do if test -f $out/http.ready; then break; fi sleep 0.05 done test -f $out/http.ready for pid in ${pids[]}; do kill -0 $pid; done ip netns exec $c $busy nslookup -typeA service.test 192.0.2.1 | tee $out/dns-query.txt ip netns exec $c python3 $base/scripts/lab-http-layers.py probe $out echo Evidence: $out文件scripts/lab-http-layers.pyimport http.client import ipaddress import json import re import socket import sys from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path class Handler(BaseHTTPRequestHandler): def do_GET(self): cases {/: (200, blab-ok), /private: (403, blab-denied), /wrong-result: (200, blab-error)} status, body cases.get(self.path, (404, blab-missing)) self.send_response(status) self.send_header(Content-Type, text/plain) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) mode, directory sys.argv[1:] out Path(directory) if mode serve: with HTTPServer((192.0.2.1, 8080), Handler) as server: (out / http.ready).touch() server.serve_forever() elif mode probe: dns_output (out / dns-query.txt).read_text() match re.search(rName:\sservice\.test\sAddress:\s(\d\.\d\.\d\.\d), dns_output) if not match: raise SystemExit(No expected A answer from DNS tool) address str(ipaddress.IPv4Address(match.group(1))) assert address 192.0.2.1 results {resolved_address: address, cases: []} try: with socket.create_connection((address, 8081), timeout2): raise AssertionError(Closed lab port unexpectedly accepted connection) except ConnectionRefusedError: results[cases].append({port: 8081, tcp: refused, http: None}) for path, expected_status, expected_body in ( (/, 200, lab-ok), (/private, 403, lab-denied), (/wrong-result, 200, lab-error)): connection http.client.HTTPConnection(address, 8080, timeout2) try: connection.connect() peer connection.sock.getpeername() connection.request(GET, path, headers{Host: service.test:8080}) response connection.getresponse() body response.read().decode(ascii) assert response.status expected_status assert body expected_body results[cases].append({port: 8080, path: path, tcp: connected, peer: list(peer), http: response.status, body: body, expected_business_value: body lab-ok}) finally: connection.close() results[passed] True results[scope] isolated plaintext HTTP; explicit A lookup, no browser/TLS/proxy test (out / result.json).write_text(json.dumps(results, indent2)) print(json.dumps(results, indent2)) else: raise SystemExit(Unknown mode)运行与结果判断在上述工作目录执行bash scripts/prepare-network-lab-tools.sh sudo bash scripts/verify-network-http-layers.sh输出保存在工作目录的 data/verification 下以运行时间命名。正常完成会显示断言通过还应检查本次目录的 result.json 中 passed 为 true。预设的失败对照属于实验内容不意味着脚本整体失败依赖缺失、权限不足或断言失败则不能算通过。不要使用 python -O 或 PYTHONOPTIMIZE 禁用断言。脚本只修改自己新建的随机名称网络命名空间正常退出及普通错误退出会执行清理。断电、强制终止或 kill -9 不能保证清理可用 ip netns list 检查只处理确认属于本次实验的资源不要批量删除其他命名空间。输出中的地址来自隔离实验不需要填写家庭账号、真实设备地址或认证信息。
返回列表