ARTICLE DETAIL

资讯详情

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

Android 10 DHCP排查指南:从NetworkStack到dumpsys源码定位

Android 10 DHCP排查指南:从NetworkStack到dumpsys源码定位 Android 上排查“Wi-Fi已经连上但就是进不了网页”“手机开热点另一台设备却拿不到IP地址”这类问题很多人第一反应是怀疑路由器或运营商网络但实际在Android 10之后一台手机里已经完整内置了DHCP客户端和服务端两套实现很多故障的根源就藏在这套实现里。Android 10把DHCP相关代码从原来零散的netd、dnsmasq等模块统一收进了独立的NetworkStack模块调试入口也从过去靠感觉翻日志变成了从dumpsys network_stack出发一路追踪到DhcpServer.java、DhcpClient.java源码的标准化流程。这篇文章我不讲理论书上的DHCP报文格式而是按实际干活时的排查顺序把从命令查询到源码阅读的完整链路拆开讲一遍也把这两年踩过的坑一并交代清楚。适合正在做系统定制、网络方案集成或者运营商问题反馈的Android工程师以及那些在路由器和终端之间来回跳转却始终定位不了问题的人。1. 先搞懂Android 10的DHCP代码到底在哪1.1 独立出来的NetworkStack模块Android 10之前DHCP功能散落在好几个地方客户端相关逻辑在netd里热点和服务端相关的DHCP分配往往交给dnsmasq进程框架层再有各种状态机去调度。那时候调试一个问题经常要同时开三四个终端adb shell ps -A里看到dnsmasq和netd都有嫌疑日志一多根本分不清谁是谁。Android 10把这个局面彻底改了。与DHCP客户端、DHCP服务器、IP地址分配、DHCP租约存储直接相关的一套代码统一集中到NetworkStack模块里编译出来是一个系统级APK进程名通常是com.android.networkstack。在AOSP源码中的路径是packages/modules/NetworkStack/核心文件就是DhcpClient.java和DhcpServer.java两个再加上一批DhcpPacket.java、DhcpClientStateMachine.java之类的辅助类。这个模块虽然是APK形式但它并不像普通App那样运行在应用沙箱里而是作为系统服务进程存在同时被Wi-Fi、以太网、热点Tethering等子系统调用。这一改动最大的好处是DHCP相关日志、状态、进程边界都收敛了。过去那种“在netd里看到了但在dnsmasq里好像也有”的纠结基本消失调起问题来思路清晰得多。1.2 DHCP在Android 10里涉及哪些进程和端口虽然代码统一了但实际运行的时候依然存在多进程配合。STA模式下手机作为DHCP客户端由NetworkStack进程内的DhcpClient负责发Discover、收Offer、发Request、收ACK整个过程走标准的UDP 67服务端、68客户端端口。热点/软AP模式下NetworkStack进程内的DhcpServer监听热点网卡通常叫ap0或wlan0出接口监听UDP 67端口给连接热点的设备分配地址。如果手机走USB网络共享USB TetheringDHCP服务器同样会出现在rndis0这类的网卡接口上。所以排查时要先明确一个前提你的问题到底是客户端侧还是服务端侧。客户端拿不到IP、续租报错大概率要盯DhcpClient那条链路热点或者USB共享下的设备拿不到IP要看DhcpServer是不是在dumpsys输出里正常注册了对应接口。很多人卡在这里一是不知道这两个进程实际跑在同一个com.android.networkstack进程里二是不知道这个进程里其实分了两套完全独立的状态机。另外一个容易忽视的点是Android 10之后dnsmasq在默认通路里不再承担热点DHCP服务器的职责但部分厂商定制ROM或者服务器固件场景下仍然会存在一个tethering相关进程去调用底层脚本。如果你拔掉热点后ps -A | grep dnsmasq还能看到进程那说明这台机器可能走了兼容路径这时候按纯Java实现去排查就会对不上号。遇到这种情况我建议先确认系统用的到底是Java版DhcpServer还是dnsmasq版本别拿到一台设备就开始套命令。2. dumpsys network_stack实操现场第一步就是它2.1 命令怎么敲输出长什么样进入Android 10之后NetworkStack注册了自己的系统服务dumpsys名称就是network_stack。排查DHCP问题命令行第一站就是adb shell dumpsys network_stack如果觉得输出太长可以加时间限制adb shell dumpsys -t 5 network_stack还有一个前序动作先确认服务是否真的在运行adb shell dumpsys -l | grep network能看到类似network_stack、wifi、ethernet、connectivity这样的服务名说明网络栈服务已经正常注册。不同Android版本、不同厂商ROM对dumpsys network_stack的输出格式有很大差异有些展示的是整个NetworkStack的概况有些分客户端和服务端两个大块。我手头一台相对接近原生Android 10的设备核心输出长这样NetworkStack service Client interfaces: interface: wlan0 state: CONNECTED ip: 192.168.1.23/24 gateway: 192.168.1.1 lease duration: 86400 dns: [192.168.1.1, 114.114.114.114] dhcp state: BOUND Server interfaces: interface: ap0 subnet: 192.168.43.0/24 server ip: 192.168.43.1 range start: 192.168.43.10 range end: 192.168.43.250 lease time: 7200 active lease count: 3以上是简化的模拟输出不代表所有ROM一致但几个关键字段基本都能找到每张网卡当前走的DHCP状态、服务器监听的接口、分配的地址池、租约时间、当前活跃租约数。在真实设备上如果恰好是自定义ROM可能还会出现DhcpStats、PacketStatistics、LastError之类的字段这些字段往往是厂商为了方便售后排查加的利用好了能省一半时间。2.2 从输出里快速定位异常拿到dumpsys network_stack的输出后不要从头到尾逐行读直接按下面四条去扫第一看Client interfaces下有没有你正在使用的网卡。如果网卡本身没出现在这个节点里说明DhcpClient根本没有为该网卡启动或者该网卡走了静态IP配置问题跟DHCP无关而去Settings里查看IP分配方式是DHCP还是Static。第二看dhcp state字段。出现BOUND说明地址已获取出现INIT、SELECTING、REQUESTING、RENEWING、REBINDING分别对应状态机的不同阶段。卡在REQUESTING和RENEWING最常见。REQUESTING一直不跳转到BOUND多数是服务端没有正确回复ACKRENEWING阶段反复失败则要重点怀疑租约表以及网关ARP交互是否正常。第三看Server interfaces下的盘点有没有预期接口。开热点后这里应该出现ap0或者wlan0对应的DHCP服务配置。如果没有任何服务端接口那问题根本不在DHCP上而在Tethering开启流程上Tethering都没把接口交给DhcpServer。第四看active lease count与实际连接设备数是否一致。热点开了dumpsys里却显示0个租约要么设备没真正连上热点要么连上了但DHCP请求就没到达这个进程需要继续抓包看报文是否送达。看完这四个点问题差不多就能框到具体的一段路径上要么客户端网卡没起来要么状态机卡住要么服务端没有响应请求。后面再开始翻日志排错信息就有了方向。3. 深入DhcpServer.java核心逻辑别再只靠猜3.1 源码位置与关键类地图dumpsys输出能让你知道问题在哪一层想继续往下追就必须看源码。DhcpServer.java在AOSP中的路径是packages/modules/NetworkStack/src/com/android/networkstack/dhcp/DhcpServer.java同目录下还有几个兄弟文件命名很直观DhcpClient.java客户端状态机与报文收发。DhcpPacket.java各类DHCP报文的解析与封装在AOSP中会根据OpCode翻出DiscoverPacket、OfferPacket、RequestPacket、AckPacket、NakPacket等子类。DhcpClientStateMachine.java客户端状态机里面能看到StateMachine的各个状态定义。DhcpServerLeaseRepository.java服务端租约存储负责记录当前已分配的IP、过期时间、MAC地址绑定。DhcpServerHandler.java老版本里服务端的消息处理器负责将网络层收到的原始数据包交给DhcpPacket解析再交给具体逻辑分发。Android 10之后这个目录下代码做了不少结构重组DhcpServer本体的run()方法里不再直接写死所有逻辑而是把事件通过Handler分发。读源码时要抓住两条主线一条是报文接收解析链一条是租约分配链路。连上之后查看onCreate()里的makeDhcpServer工厂方法、start()启动逻辑、handleDiscover/handleRequest处理逻辑这几个点是理解服务端行为最快的抓手。3.2 核心报文处理流程解析DhcpServer.java的run()方法创建了一个DatagramSocket绑定到接口地址的UDP 67端口然后进入while循环不断调用receive()接收来自客户端的DHCP数据包。收到数据后代码会调用DhcpPacket.decodeFullPacket()把原始字节解析成结构化对象。按照标准DHCP流程服务端先处理DHCPDISCOVER。发现请求后DhcpServer会从地址池里挑一个未被占用的IP构建一个OFFER报文填入客户端的MAC、事务IDxid、建议IP、子网掩码、网关、DNS、租约时间这些选项然后从同一个socket回给客户端。这个阶段不真正占用地址只是“预分配”给谁、给多久都是临时状态。接着客户端发来DHCPREQUEST。服务端在handleRequest里要做几件事解析客户端想要哪个IP校验这个IP是否还在地址池范围内校验是否已经分配给了别的MAC全部通过后生成DHCPACK同时把这条租约写进DhcpServerLeaseRepository。如果校验失败比如客户端本来是通过中继代理来的但中继地址跟服务端子网对不上或者IP已经被其他设备占用则回复DHCPNAK。服务端还有一个容易被忽略的流程处理DHCPRELEASE和DHCPDECLINE。客户端主动释放IP时发送DHCPRELEASE服务端删除租约并回收地址客户端检测到地址被占用时会发DHCPDECLINE服务端要把这个IP从池中标记为“脏地址”避免再次分配出去。实际调试中如果发现地址总是重复冲突可以重点看DECLINE报文的数量如果这个数值很高往往不是服务端的问题而是客户端自身ARP检测机制过于敏感。3.3 几个必须掌握的静态方法读DhcpServer.java时以下几类方法是你最常打日志和断点的地方makeDhcpServer(...)创建服务端实例传入接口名、子网对象、地址池配置。run()主循环这就是服务端线程的入口。handleDiscover/handleRequest控制OFFER和ACK/NAK逻辑。buildAckPacket/buildOfferPacket组装返回报文。leaseRepository.toString()把当前租约表输出成字符串调试时可以直接打印到logcat里看。shutdown()关闭socket、清理租约。另外DhcpPacket里有一个decodeFullPacket静态方法它在每个报文进来时都会被调用。如果你想看所有进来的报文长什么样在decodeFullPacket返回值之后打一行Log.d(DhcpServer, packet.toString())就能在logcat里看到完整字段。这个改动虽然要重新编译模块但说实话比抓包还直观。4. 日志与抓包把证据链抓到手4.1 开启详细日志的正确姿势源码级调试第一步是打开详细日志。NetworkStack内部大量使用Log.d级别的日志默认情况下release包是关闭的。要打开可以在root过的设备上执行adb shell setprop log.tag.DhcpServer VERBOSE adb shell setprop log.tag.DhcpClient VERBOSE adb shell setprop log.tag.NetworkStack VERBOSE adb shell setprop log.tag.DhcpPacket VERBOSE设置后重新触发一次Wi-Fi重连或者热点重开再抓logcatadb logcat -v threadtime | grep -E DhcpServer|DhcpClient|DhcpPacket|NetworkStack如果你不想抓全量也可以先清空缓冲再复现adb logcat -c # 触发问题 adb logcat -d -v threadtime | grep -E DhcpServer|DhcpClient注意log.tag的优先级是VERBOSE DEBUG INFO只有一个VERBOSE能打开所有级别。部分定制ROM会屏蔽persist.log.tag的修改这时就要用adb root后直接改/system/etc/prop.default或者在init.rc里加一行属性再重启生效。4.2 抓包配合Wireshark才是王道日志只能告诉你进程内部发生了什么报文在网络层到底有没有到达还得靠抓包。Android 10上最简单的方式是用系统自带tcpdump如果有或安装一个可以用root权限抓包的工具adb shell tcpdump -i any -n -s 0 -w /data/local/tmp/dhcp.pcap udp port 67 or udp port 68在另一个终端里触发重连或重启热点复现问题后按CtrlC停止抓包然后adb pull /data/local/tmp/dhcp.pcap ./dhcp.pcap打开Wireshark过滤规则填bootp or dhcpWireshark会把DHCP报文解析得很漂亮。我最常用的一组过滤表达式只看Discoverdhcp.msg.type 1只看Offerdhcp.msg.type 2只看Requestdhcp.msg.type 3只看ACK/NAKdhcp.msg.type 5 || dhcp.msg.type 6抓包有个技巧不要只抓Android设备本机的包如果有条件最好在路由交换设备或AP上同时抓一份。因为有些问题不是Android不发送报文而是报文到了无线侧就被丢弃了。比如某些企业级AP开了DHCP Snooping但信任口配置错误导致DHCP报文直接被交换机丢弃这种问题你在手机侧怎么抓都看不到服务端回应换到AP上抓才看到报文压根没过去。这也是为什么一些一体化的排查工具里中继、Snooping配置会成为必查项。5. 高频故障排查实录5.1 场景一Wi-Fi连上却拿不到IP地址这是最常见的故障。dumpsys network_stack里Client接口在线状态却长期卡在SELECTING或REQUESTING。排查步骤我先固定下来dumpsys network_stack看客户端状态确认是INIT还是SELECTING。拉起logcat确认DhcpClient有没有发出Discover报文。抓包确认Discover是否到了网络侧。看服务端有没有Offer回包。如果logcat里完全没有任何DhcpClient的报文日志大概率问题在Wi-Fi连接本身或者客户端预期的目标网段和实际网络不一致。有一版国内定制ROM在Wi-Fi连接成功后系统会异步等待ConnectivityService的网络评分结果导致DhcpClient延迟启动表现就是连上Wi-Fi后半分钟才有IP。这个在纯Android代码里很难一眼看出来得结合dumpsys connectivity里的NetworkAgentInfo一起看确认网络是否被标记为VALIDATED。如果Discover发出去了但收不到Offer就要区分两种情况。一种是无线路由器本身没有开DHCP Server或者把DHCP服务绑定到了错误的VLAN。另一种是中间网络设备丢包比如前面提到的DHCP Snooping过滤、端口隔离、IGMP Snooping里存在错误组播设置等。我遇到过一例是路由器开启了“DHCP中继全局模式”但中继地址指向了空地址所有Offer都被转去了不存在的网络终端自然等不到。5.2 场景二拿到IP后过几分钟就断网或者续租失败这种故障最典型的地方在RENEWING阶段。DHCP租约是有生存期的默认路由器的租约可能是24小时但Android热点默认短一些。当租约过半客户端会进入RENEWING状态向原服务端单播续租请求。dumpsys network_stack里如果一直卡在RENEWING并且logcat里反复打印Unable to renew lease之类的日志建议先查时间戳。我排过一个大项目就是终端系统时间与路由器相差了8小时导致续租请求里的剩余租约期直接被服务端判定为异常NAK回去重新走完整Discover流程。这种问题不看报文完全没法发现抓包后看到Offer里带的时间戳和本地相差一大截才能对症。还有一类续租失败是地址池耗尽。地址池通常只是192.168.43.10到192.168.43.250如果前一期设备没有正常释放租约池子慢慢被占光新设备再来就分不到地址。此时dumpsys network_stack里active lease count会接近池子上限把租约时间改短、清理一次租约缓存即可恢复。在部分ROM里清空热点缓存可以直接去设置-系统-重置-重置网络设置但代价是Wi-Fi、蓝牙配置全被清掉操作要谨慎。5.3 场景三热点开了但设备连上没有IP这是服务端问题的高发场景。首先做一个快速二分法热点打开后adb shell dumpsys network_stack里有没有出现Server interfaces没有说明Tethering没有成功把接口交给DhcpServer。有但设备连上后没有租约再看接口名是否与设备实际连接的热点接口一致。有一次我排查一个“连热点显示已保存但无法访问互联网”的问题dumpsys network_stack里服务端绑定的接口是wlan0可实际热点创建的虚拟接口是ap0。原因是厂商在Wi-Fi驱动配置里把AP接口的主接口名写错了导致DHCP Server监听在一个无流量网卡上。这个问题在代码里看不出来必须对比dumpsys wifi里的热点接口名和dumpsys network_stack里的Server接口名。另一次问题在域名非IP地址分配本身。设备拿到了192.168.43.x的IP能ping通但打不开网页。这个其实不是DHCP服务端的问题而是Tethering进程把DNS请求代理到了系统里一个未启动的服务。但让我意外的是DhcpServer里的DNS配置选项没有错问题出在Android的netd里DNS转发规则。所以提醒一句遇到“拿到地址但上不了网”别只盯着DHCP先确认IP、网关、DNS三层各自正常。5.4 场景四涉及DHCP中继、跨VLAN区域的坑手机做热点大多直接接入局域网但企业项目里经常会把Android设备当无线接入点后面挂交换机和路由器。这时候DHCP报文要跨网段必须要在中间路由器上配置DHCP中继DHCP Relay。中继配置常见的也是两种写法接口模式下指定Global DHCP服务器地址类似dhcp select global。指定具体中继地址把客户端的Discover广播报文转换成Unicast发到服务端再把服务端应答带回客户端。如果中继配置不对症状和前面很像客户端不断发Discover但收不到Offer。但注意这里的问题既不在客户端也不在服务端而在中间的交换路由设备。排查方法很简单抓包看giaddr字段。Discover报文如果到了服务器报文头里的giaddr应该被中继设备改写为中继接口IP。如果服务器回包时giaddr为空或者指向了错误网段客户端肯定瞎掉。这个场景还牵出一个经验很多工程师在跨VLAN环境里习惯性地给中继接口配上全局DHCP策略一旦服务端地址写错排查方向就全往Android侧跑。我建议每次遇到“多网段拿不到地址”时先在服务器端抓包看有没有收到来自目标网段的Discover有就说明中继链路基本OK没有才回头查Android侧。6. 实战经验与压箱底技巧6.1 一套可复用的排查顺序我把自己在项目中反复验证过的排查顺序整理成一张速查表能覆盖70%以上Android 10 DHCP问题步骤命令/操作预期结果异常时说明什么1adb shell dumpsys -l | grep network能看到network_stack服务没起来问题可能在系统启动阶段2adb shell dumpsys network_stack能看到Client/Server接口和状态看不到接口则系统没有尝试启动DHCP3adb shell dumpsys wifi | grep -i interface热点接口名与Server接口一致接口不符多发生在厂商驱动改动上4打开logcat详细日志能看到Discover/Offer日志日志为空则报文未到达客户端代码5tcpdump抓包能看到双向DHCP报文单侧看不到问题在网络链路不在设备6Wireshark里面看giaddr和option字段字段值符合子网规划中继配置错误或服务端选项有误这套顺序的核心思想是从服务到接口从接口到日志从日志到报文一层一层把范围缩小而不是上来就翻源码。多数问题在这一层就能定位只有真正需要改代码的场景才需要进DhcpServer.java。6.2 改了源码怎么快速替换验证源码级的改动需要重新编译NetworkStack模块。在AOSP环境下source build/envsetup.sh lunch 你的产品名-userdebug make NetworkStack编译好的APK一般在out/target/product/产品名/system/priv-app/NetworkStack/NetworkStack.apk。替换时有两种方式。一种是直接push进系统目录适合userdebug或者eng版本adb root adb remount adb push NetworkStack.apk /system/priv-app/NetworkStack/NetworkStack.apk adb shell chmod 644 /system/priv-app/NetworkStack/NetworkStack.apk adb reboot另一种是用adb install -r直接覆盖安装但系统APK通常有签名校验普通安装方式可能不生效。要加快验证效率我习惯在代码里埋一个临时的“开关属性”在DhcpServer启动时读取一个系统属性比如persist.vendor.dhcp.debug如果值为1就打开额外日志和租约表打印这样替换一次APK就可以长时间在线分析不必每次改代码都重编。有一点要特别注意NetworkStack模块作为系统核心网络组件替换失败会导致Wi-Fi/移动网络/蓝牙全部异常甚至开不了机。所以动手前一定要备份原APK做OTA升级场景的还需要确认系统分区空间足够。6.3 三个不容易想到但很管用的细节最后分享三个源码调试的小细节都是我实际摸黑踩出来的。第一个关于DhcpServer.java里的租约存储。Android 10的租约默认保持在/data/misc/目录不同厂商可能落在不同的子目录里。需要快速查看租约时别去猜路径直接在logcat里打印leaseRepository对象的内容或者在源码里加一行Log.d(DhcpServer, leaseRepository.toString())一了百了。第二个关于系统属性开关。很多工程师会随手把log.tag.DhcpServer设为VERBOSE但Android 10对log.tag.*有长度限制和缓存限制部分字段设置后在热点重启后会被重置。我的做法是把属性写到/data/local.prop或者vendor分区里的prop文件中确保重启后仍生效。第三个关于抓包工具与DHCP选项里的HostName。很多Android终端在DHCP请求里带了HostName字段路由器会把主机名登记到设备列表。如果你在路由器后台看到的主机名全是Android不要奇怪这是正常的也正因为这样用dumpsys按MAC地址去匹配租约比按主机名过滤可靠得多。改主机名只能在系统层做应用层是改不到DhcpClient发出的HostName字段的。另外遇到疑似地址冲突时别忽略ARP层。DHCP Server分出去的IP只是“建议分配”客户端拿到IP后还可能会做冲突检测ARP Probe。如果同一网段里有另一台设备占了同一个IP客户端会发DECLINE服务端将这个IP标记为冲突。此时再从地址池里继续分配可能连环触发冲突。最快的定位办法就是用抓包同时过滤arp和bootp把两个协议的时间线对照着看。最后再补一个建议。排查Android 10的DHCP问题时建议在电脑上固定备好三样东西dumpsys network_stack的输出模板、DhcpServer.java和DhcpClient.java的索引文件、一份Wireshark过滤语法速查表。这三大件能让你在接到用户反馈的第一时间就直接进入取证状态而不是在面对一串串晦涩输出时愣在当场。DHCP问题看起来都是“拿不到地址”这一个症状但根源可能分布在驱动、框架、报文、路由好几个层面每个层面只要多花几分钟做一次标准动作定位到根因的时间就能缩短一大半。
返回列表