拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Android 10 DHCP问题排查:从dumpsys到DhcpServer源码

Android 10 DHCP问题排查:从dumpsys到DhcpServer源码

拿到一台Android 10的设备,用户反馈“插上网线拿不到IP”,或者Wi-Fi信号满格但就是上不了网,这类问题十有八九要落在DHCP流程上。Android 10这一代刚好是网络栈换血的关键节点,旧版依赖的外部dhcpcd进程在AOSP里逐步被内置的DhcpServer替换,从dumpsys network_stack到DhcpServer.java,调试工具链和思路跟以前完全不一样。这篇文章我把实际排查过程中用到的方法、命令、源码埋点和踩过的坑完整梳理一遍,希望能给正在被Android 10 DHCP问题折磨的人一点参考。

1. 为什么现在调试Android 10的DHCP这么麻烦

1.1 从dhcpcd到DhcpServer:Android网络栈的第一次大换血

Android 10之前,DHCP客户端的实现主要靠一个独立的守护进程dhcpcd,它是从Linux生态移植过来的。你可以在adb shell里直接ps -A | grep dhcp,能看到它的进程,日志也存在logcat里,tag基本都是DhcpClient。排查这类问题基本三板斧:看dhcpcd有没有起来,看logcat里有没有dhcp相关的报错,再不行就tcpdump抓包。整体思路比较线性,跟排查普通Linux服务器上的DHCP问题差别不大。

到了Android 10,情况变了。AOSP开始把网络相关的东西收拢成一个名叫NetworkStack的系统模块,DHCP客户端从外部进程慢慢迁移到Android框架进程内部实现。这个过程中诞生了一个新类,就是标题里提到的DhcpServer.java,位置在packages/modules/NetworkStack/src/com/android/networkstack/dhcp/DhcpServer.java。它不仅仅是把旧代码翻新,而是直接在Java层实现了一个完整的DHCP服务器逻辑,比如构造DHCPOFFER、处理DHCPREQUEST、管理租约等。

正是因为这次架构调整,调试手段也得跟着升级。以前ps一下就能确认进程存在,现在你得学会dumpsys network_stack;以前logcat里直接搜dhcpcd,现在得知道NetworkStack进程里的tag到底叫什么;更麻烦的是,如果你需要给DHCP服务器加日志、改逻辑,还得重新编译整个NetworkStack APK再推到系统里,跟以前改个dhcpcd配置文件完全是两码事。很多人第一次接触Android 10的DHCP问题,就是栽在这个“找不着北”的阶段。

1.2 一套通用的调试出发思路

其实不管架构怎么变,DHCP问题的本质没变,就是四个报文的你来我往:客户端发DHCPDISCOVER,服务器回DHCPOFFER,客户端再发DHCPREQUEST确认,服务器最后回DHCPACK。任何一步断了,表象都是拿不到IP。所以我的调试思路一直都遵循一个固定套路:先确认服务端“活了没”,再确认报文“到没到、回没回”,最后才是“代码里到底发生了什么”。

对应到Android 10上,这三步分别就是:

  • 用dumpsys network_stack查看NetworkStack服务的运行状态、接口状态、各客户端注册情况;
  • 用logcat过滤NetworkStack、DhcpServer相关日志,同时用tcpdump抓包确认报文交互;
  • 如果前两步都正常但问题依旧,那就得进源码,在DhcpServer.java里加埋点日志,重新编译验证。

这套思路的好处是每一步都建立在前面一步的事实基础上,不会一上来就乱改代码。很多新手拿到问题喜欢直接翻源码,但DHCP这种网络问题,报文层面的证据往往比代码逻辑更能说明问题。我见过太多“代码看起来没问题,实际是报文根本没到”的案例了,所以先把链路打通再往下钻,永远是最省时间的路径。

2. dumpsys network_stack:先看清楚服务端活没活

2.1 命令正确打开方式与输出解读

拿到一台Android 10设备,第一步我建议先敲下面这条命令:

adb shell dumpsys network_stack

这个命令会把NetworkStack进程内部的所有状态全部dump出来,内容非常多,但不要被吓到。我一般不会从头看到尾,而是用grep先做第一轮粗筛:

adb shell dumpsys network_stack | grep -i dhcp adb shell dumpsys network_stack | grep -iE "interface|eth0|wlan0"

第一次执行的时候你可能会遇到一个常见问题:dumpsys network_stack输出为空,或者提示找不到服务。这种情况大概率是设备上没有跑NetworkStack这个服务,或者服务异常退出了。正常的Android 10设备上,这个服务应该一直在后台运行,因为它承载了以太网、Wi-Fi的IP配置管理职责。如果服务不存在,那问题已经找到一半了,往system_server的崩溃日志方向查。

输出里你需要重点关注几个区块。下面是我在一台正常设备上抓到的关键信息片段,我做了精简:

NetworkStack Service: mRegisteredClients: InterfaceState{interfaceName=eth0, ...} IpClient{...} mIpClientState=CONNECTED mDhcpServerState=STARTED mServerAddress=/192.168.50.1 mLeases={...}

看到mDhcpServerState=STARTED,说明这台设备的以太网口上确实启动了内置DHCP服务器,这个状态字段非常关键。如果这里显示的是IDLE或者STOPPED,那就说明服务端压根没把DHCP服务器跑起来,问题根源就锁定在这一层,不用再去管客户端收没收到报文了。

2.2 几个必看的核心字段与其含义

为了不让大家面对输出发懵,我把dumpsys network_stack输出里和DHCP强相关的几个核心字段整理成了一张表,方便对照着看:

字段常见值含义与判断思路
interfaceNameeth0 / wlan0当前IP配置适用的网络接口,确认是不是你在排查的那张网卡
mIpClientStateCONNECTED / DISCONNECTED / STOPPEDIpClient整体的连接状态,DISCONNECTED说明还没完成配置
mDhcpServerStateSTARTED / IDLE / STOPPED内置DHCP服务器是否在跑,IDLE往往是没拿到服务器地址或者接口未就绪
mServerAddress192.168.x.xDHCP服务器自身的IP,这个值决定了OFFER报文里会填什么来源地址
mLeasesMAC->IP绑定列表已分配的租约记录,客户端如果在这里能看到自己的MAC,说明服务器已经分配过IP
mRegisteredClients接口列表注册到NetworkStack服务的接口集合,如果接口都没注册,后面的所有逻辑都不会执行

每次dump完,我建议先把这几个字段抄出来,再决定下一步。举个例子,如果mDhcpServerState是IDLE,而mServerAddress为空,那基本可以判断是服务器没有拿到一个合法的静态地址来作为DHCP的锚点,最常见的原因是接口还没被配成静态IP就尝试启动DHCP服务了。这时候光看logcat可能绕很久,dumpsys一眼就能拨开迷雾。

2.3 状态不对时的第一反应

dumpsys network_stack的输出就像体检报告,指标不对,第一步肯定是找“病根”在哪层。我的经验是:如果mDhcpServerState不是STARTED,先别急着去改动DhcpServer.java,而是要把接口状态、网络配置来源这两件事同步确认掉。

接口状态用ifconfig或者ip addr查:

adb shell ifconfig eth0 adb shell ip addr show eth0

网络配置来源就要看法则是手工静态配置,还是通过EthernetTracker从系统设置里读出来的。在Android 10的以太网配置里,如果管理员把“IP设置”定成了静态,并且填了192.168.x.x的地址,那么NetworkStack才有可能在这个接口上启动DHCP服务器。如果配置来源是DHCP客户端模式,那mDhcpServerState长期停留在IDLE就很正常,因为此时这台设备是作为客户端去外面获取IP的,压根不应该在这里开服务器。

很多人在这一步会忽略一个很关键的事情——时间顺序。打开dumpsys看到当前是IDLE,不等于它从来没启动过。我踩过一次坑,某台设备启动早期DHCP服务器是正常STARTED的,但跑到某个阶段接口重启了,状态就掉回IDLE。所以dumpsys输出只能反映当下,如果怀疑是“启动后中途崩了”,得配合logcat看启动时序,不能只看一次dump就下结论。

3. 跟进日志与抓包:把“看不见”的报文还原成证据

3.1 logcat 过滤规则与关键日志

dumpsys告诉你服务端当前状态,但无法告诉你报文交互的过程是否顺畅,这一层要靠日志和抓包来补。Android 10的NetworkStack把核心的DHCP逻辑都跑在NetworkStack进程里,对应的日志tag你需要记住几个:NetworkStack、DhcpServer、IpClient、EthernetTracker。

下面是我常用的过滤命令:

adb logcat -v time -s NetworkStack:V DhcpServer:V IpClient:V EthernetTracker:V

如果设备上logcat默认把verbose级别过滤掉了,可以用*:S把所有输出静音,再按tag打开:

adb logcat -v time NetworkStack:V DhcpServer:V *:S

正常流程下,你会在日志里看到类似这样的序列:

DhcpServer: Starting DHCP server on eth0 DhcpServer: Received DISCOVER from xx:xx:xx:xx:xx:xx DhcpServer: Sending OFFER with ip 192.168.50.100 DhcpServer: Received REQUEST from xx:xx:xx:xx:xx:xx DhcpServer: Sending ACK to 192.168.50.100

这里有个经验要分享:如果你看到“Received DISCOVER”,但后面没有“Sending OFFER”,那问题大概率出在服务器处理逻辑内部,比如租约池满了、地址冲突检测没过、或者报文解析抛了异常。如果连“Received DISCOVER”都没有,那就要回头去查链路层的东西了,比如网线、交换机端口、VLAN配置等,因为报文压根没进到Android系统里。

3.2 tcpdump 抓包实操与DHCP交互流程对照

排查网络协议问题,抓包永远是终极武器。Android 10的root设备上,tcpdump可以直接用,前提是系统里有这个二进制文件。没有的话可以用adb push一个静态编译版本进去:

adb root adb remount adb push tcpdump /system/bin/ adb shell chmod 755 /system/bin/tcpdump

抓包命令和思路跟Linux下基本一致:

adb shell "tcpdump -i eth0 -s 0 -w /data/local/tmp/dhcp.pcap port 67 or port 68"

这里提个细节,很多人喜欢加-vv参数想把报文内容打全,但我实际使用中更倾向于把pcap导出来用Wireshark分析。因为DHCP报文每个字段都有官方名称和含义,Wireshark里一眼就能看到option 53(消息类型)、option 55(请求的参数列表)、option 61(客户端ID)等关键信息,比在终端里硬读十六进制效率高太多。

抓完把文件导出:

adb pull /data/local/tmp/dhcp.pcap

用Wireshark打开,设置过滤条件dhcp即可。这时候你就能清晰地看到四条报文的来回顺序了。

3.3 用日志+报文定位“不发Offer”的常见原因

结合日志和抓包,基本能对所有“发不出Offer”的情况做归类。我把最常见的几种原因和对应的表象整理一下:

根因方向日志表现报文表现
服务器地址未配置DhcpServer缺少server address,启动失败无任何DHCP响应
租约池已满日志提示no available leaseDISCOVER发出后无OFFER
接口状态异常IpClient报接口down或address缺失DISCOVER发不到服务器
报文被交换机/iptables丢无日志抓包只看得到DISCOVER,没有上行包
客户端ID不合法日志解析异常DISCOVER报文option 61格式错误

遇到“DISCOVER发出去、OFFER回不来”这类情况,我个人的排查习惯是:先在Android侧抓包确认OFFER到底有没有从网卡发出去。如果在Android系统的出口已经看到OFFER,但客户端收不到,那就是中途链路的问题,跟Android无关;如果Android侧压根没发出OFFER,再回logcat看服务器处理逻辑到底卡在哪一步。

4. 读源码改日志:拿到DhcpServer.java的第一手信息

4.1 源码位置与工程结构

前两招用完之后,如果问题还没解决,或者你明确需要改一些默认行为(比如租约时间、地址池范围),那就到了请出DhcpServer.java的时候。Android 10的NetworkStack源码路径在AOSP里是这样排布的:

packages/modules/NetworkStack/ ├── src/com/android/networkstack/ │ ├── dhcp/ │ │ ├── DhcpServer.java │ │ ├── DhcpServerEventHandler.java │ │ └── DhcpPacket.java │ ├── apishim/ │ └── NetworkStackService.java └── tests/

其中DhcpServer.java是整个DHCP服务器的核心调度类,它负责监听UDP 67端口、解析收到的DHCP报文、生成并回送响应报文。如果只是排查问题,我通常会先在这个类里加日志,而不是直接改业务逻辑。记住,先定位再动手,是改动系统代码的基本素养。

如果要编辑源码,Android Studio可以直接打开packages/modules/NetworkStack这个目录作为工程,但更常见的做法是直接在服务端代码树里改,然后用命令行编译。个人开发机上没有完整Android源码的话,也可以单独构建NetworkStack模块,前提是你已经下载过对应版本的AOSP代码。

4.2 关键方法与埋点选择

打开DhcpServer.java,你首先会看到这样一个类注释和核心成员变量:

public class DhcpServer { private static final String TAG = "DhcpServer"; private final ServerSocket mServerSocket; private final Map<String, DhcpLease> mLeases = new HashMap<>(); ... }

注意TAG的值是DhcpServer,这就是你在logcat里要过滤的tag。整个服务器的生命周期很清晰,核心方法有这么几个:

  • start():初始化socket、绑定端口、启动接收线程;
  • handlePacket():根据报文类型分发处理,DISCOVER走offer流程,REQUEST走ack流程;
  • buildPacket():构造DHCPOFFER、DHCPACK等响应报文;
  • sendResponse():把构造好的报文从socket发出去。

我一般会在handlePacket()的入口加一条日志,把收到的报文类型源MAC等关键信息打出来:

@Override protected void handlePacket(DhcpPacket packet, ...) { Log.d(TAG, "handlePacket type=" + packet.mMessageType + " mac=" + packet.getClientMac()); ... }

如果你怀疑某个DHCP OFFER的参数不对,那就在buildPacket里把即将填充的ip地址、掩码、网关、DNS都打出来,这样能直接把问题定位到构造逻辑还是上层传参。这种“入口埋点+出口埋点”的做法,基本能覆盖90%以上的定位需求。

4.3 编译替换NetworkStack的两种方式

改完源码,接下来就是编译和替换。这里有两种常见路线,分别对应有完整源码树和只有单模块的开发者。

路线一:在完整AOSP源码树里编译整个模块

source build/envsetup.sh lunch <你的设备对应的平台> cd packages/modules/NetworkStack mm

编译产物通常在out/target/product/<设备>/system/framework/下面,模块名可能是NetworkStack.apk。然后重新打包system.img刷机,或者用adb push把APK推到system/framework里再重启。要注意的是,这种替换方式要求设备的dm-verity处于关闭状态,一般userdebug版本比较方便。

路线二:单独编译APK再push

如果你只改了DhcpServer.java一个小文件,也可以单独构建APK:

cd packages/modules/NetworkStack make NetworkStack

生成的APK路径用find命令找一下即可:

find out/ -name "NetworkStack.apk"

然后用adb push覆盖系统文件:

adb root adb remount adb push NetworkStack.apk /system/framework/ adb reboot

这里有一个特别容易踩的坑:Android 10上NetworkStack是以APK形式存在的系统模块,但它还涉及权限签名,直接用debug key编译出来的APK可能签名不匹配,装上去会导致NetworkStack服务起不来。所以如果只是临时调试,我一般建议尽量用官方预签名的方案,或者确保你的编译输出是带平台签名的版本。否则你会发现设备重启后network_stack服务直接消失,场面会变得很难看。

5. 实战案例:Android 10设备获取不到IP的排查全程

5.1 场景还原与初步定位

为了帮助大家把前面讲的内容串起来,我分享一个真实的排查过程。某款基于Android 10的嵌入式设备,通过以太网口连接测试网络,路由器开了DHCP,网关是192.168.50.1,但设备Android系统里面怎么都拿不到IP。用户反馈到我们这边时,日志已经被抓了好几轮,但没找到明确的报错。

我上手第一步就是执行dumpsys network_stack,看接口状态。结果发现,这台设备的以太网卡eth0虽然在mRegisteredClients里,但IpClient一直停留在CONNECTED之前的状态,mDhcpClientState显示为DISCONNECTED,而不是预期中作为DHCP客户端应该有的状态。

这里插一句,Android 10设备本身的角色是“客户端”时,它要去外部DHCP服务器拿地址,此时它跟dumpsys network_stack里DhcpServer的一部分字段其实关系不大,要看的是IpClient这一侧的运行状态。很多内网设备跑的是“客户端模式”,但如果你dumpsys时重点放在mDhcpServerState上,方向就容易跑偏。

5.2 从报文逆向锁定“问题帧”

初步定位后,我直接在设备上抓包:

adb shell "tcpdump -i eth0 -s 0 -w /data/local/tmp/eth0.pcap port 67 or port 68"

抓到一份pcap后导入Wireshark分析,结果很有意思:DISCOVER报文确实发出去了,而且路由器也回了OFFER,但OFFER到达Android设备后,Android并没有继续回REQUEST。正常DHCP流程到这里应该继续往下走,结果却像是“收到了但没认”。

于是我把抓包里收到的OFFER报文展开,逐个option看。最后发现,路由器下发的DHCP OFFER里包含了一个Domain Search List(option 119),而Android 10的DhcpClient在解析某些格式的option 119时存在兼容性处理问题,处理失败导致整个报文被丢弃。这个根因不看到报文是完全猜不出来的,因为客户端日志几乎不会有任何输出,最多就是“收到OFFER但状态机没跳转”的沉默表现。

5.3 修复与验证

定位到问题之后,修复方案就有了明确方向。最直接的办法是调整路由器配置,把Domain Search List这个选项去掉,或者让它在合法格式内发出。但生产环境路由器不一定能改,所以我最终选择在Android侧做兼容处理,修改DhcpPacket里解析option 119的逻辑,把解析失败的异常兜住,不至于因为一个可选项导致整个报文被丢弃。

修改后重新编译NetworkStack并替换到设备上,再次执行抓包,能够看到完整的DISCOVER、OFFER、REQUEST、ACK四条报文,接口顺利拿到了192.168.50.x的地址。整个过程下来,dumpsys、抓包、源码修改各占三分之一的力气,缺一个都得走不少弯路。

6. 常见问题与排查技巧实录

6.1 常见问题速查表

以下是我在调试Android 10 DHCP过程中遇到过的典型问题,整理成速查表,方便直接对照:

现象可能原因快速排查命令/手段
dumpsys network_stack无输出NetworkStack服务未启动或崩溃logcat搜AndroidRuntime,查system_server异常
mDhcpServerState一直是IDLE接口没有合法静态地址ifconfig eth0,确认IP配置来源
DISCOVER发了没OFFER地址池满、接口状态不对、报文被链路上设备吞掉tcpdump确认设备出口是否有OFFER
收到OFFER但不回REQUEST报文选项解析异常、校验失败Wireshark逐项检查option,重点看119、55
REQUEST发了没ACK服务器端租约确认失败服务器端抓包看是否收到REQUEST
每次获取IP都很慢DHCP discover重传间隔长、超时设置不合理检查代码里的超时参数,必要时调短
连续重启设备IP老是变客户端ID不稳定检查客户端是否每次生成新的DUID

实际排查的时候,我习惯先把现象归类到“链路层、网络层、应用层”。链路层对应网线和交换机端口,网络层对应DHCP报文交互,应用层对应Android里DhcpServer/IpClient的处理逻辑。分层思考能大幅缩小排查范围,避免在错误层级上白费功力。

6.2 几条保命级别的实操经验

最后分享几条我在实际调试中摸索出来的经验,每一条都是用时间换来的。

第一,尽量保留抓包原始文件。很多人排查的时候抓完包看一眼没发现问题就删了,但DHCP协议的问题经常是间歇性的,一次抓包没抓到不代表没有。我一般会持续抓20分钟以上,确保覆盖重启、断网重连等场景,再统一分析。

第二,修改NetworkStack之前,先备份原始APK。无论是adb push还是整包刷机,都可能出现权限、签名、依赖不匹配的问题。原版APK备份好,出问题还能快速还原,不至于卡在“系统起不来”的尴尬状态。

第三,日志永远不要全开。有人喜欢logcat直接V级别全部打开,然后被汹涌的日志淹没。正确的做法是精确过滤NetworkStack、DhcpServer、IpClient这几个tag,必要时再单独开EthernetTracker。日志不是越多越好,而是关键链路的越完整越好。

第四,不要把客户端模式和服务器模式搞混。Android 10的设备既可以做DHCP客户端去外部获取地址,也可以通过内置的DhcpServer给其他设备分配地址。同一个dumpsys输出里,这两套状态都存在,排查前先分清楚你这台设备扮演的是哪个角色,不然很容易拿着服务器状态的字段去套客户端的问题,方向全反。

写在最后

Android 10的DHCP调试相比老版本确实多了一些门槛,新的NetworkStack架构、新的服务管理方式、以及新的源码结构,都需要重新适应。但把dumpsys network_stack当作入口,以抓包数据作为证据,再深入到DhcpServer.java里加日志做定点分析,这套组合拳到目前为止没有让我失望过。

我个人在实际操作中最深的体会是:DHCP这种协议层问题,70%的根因都能靠报文还原找到方向,剩下30%才是代码逻辑层面的疑难杂症。所以无论你面对的是拿不到IP、反复掉线、还是地址分配冲突,先静下心把报文链路走一遍,让事实替你说话,这比任何“魔法般”的改动都要稳妥得多。

返回列表