背景
今天接到做WIFI以后的第一个问题分析,现象如下:
复现步骤:
- 机器连接某个特定WIFI (办公场所提供,无法登陆后台确认设置信息)
- 机器再打开热点(即桥接模式)
- 手机连接热点
期望结果:
实际结果:
这个问题比较“神奇”的地方在于:
- 只有当机器连接某个特定WIFI时出现问题;
- 连接该特定WIFI时稳定复现;
- 连接其他WIFI时稳定不复现;
出于直觉,这个问题很大概率上是这个WIFI设置与机器出现了不兼容的情况;但是仍然不能解释:
机器启动热点后,分配IP地址为什么会受作为STA连接的WIFI影响?
并且,在办公区就遇到了这么一个“不兼容”的WIFI,这个概率并不能以个例解释掉;
为此,我们接下这个问题,并进行了如下分析;
分析
定位问题
由于现象是卡在“正在获取IP地址”,那么我们推测问题是处在DHCP分配IP阶段;
抓取空中包,发现DHCP报文从OFFER阶段及缺失,但是Log显示OFFER报文是正常发送了的;
进一步分析,发现由于DHCP确定分配IP的网段为192.168.42.2 ~ 192.168.42.254,这个网段是预留给USB Tethering的,因此Offer发送给了USB,而没有通过WLAN发送;因此手机端始终无法收到OFFER报文,从而无法进行后续步骤;
此外,通过连接不会复现问题的WIFI进行对比,发现正常情况下分配的IP段为192.168.43.2 ~ 192.168.43.254;
因此决定进一步分析网段部分的问题:
1. DHCP支持的网段是如何确定的:
一句话:TetheringConfiguration.java 在构造时基于资源config_tether_dhcp_range 解析而得一个字符串数组,偶数下标位为IP段的起始地址,基数下标位为截止地址,如果资源config_tether_dhcp_range 不存在,则读取默认配置,并将其一层一层传递到netd,并由netd在启动dnsmasq时,以cmdline的形式传入(--dhcp-range= );
代码路径:frameworks/base/services/core/java/com/android/server/connectivity/tethering/TetheringConfiguration.java :
private static final String[] DHCP_DEFAULT_RANGE = {
"192.168.42.2", "192.168.42.254", "192.168.43.2", "192.168.43.254",
"192.168.44.2", "192.168.44.254", "192.168.45.2", "192.168.45.254",
"192.168.46.2", "192.168.46.254", "192.168.47.2", "192.168.47.254",
"192.168.48.2", "192.168.48.254", "192.168.49.2", "192.168.49.254",
};
...
private static String[] getDhcpRanges(Context ctx) {
final String[] fromResource = getResourceStringArray(ctx, config_tether_dhcp_range);
if ((fromResource.length > 0) && (fromResource.length % 2 == 0)) {
return fromResource;
}
return copy(DHCP_DEFAULT_RANGE);
}
传递的调用栈参考如下时序图: 
2. DHCP支持多网段时是如何决定最终使用哪个网段的:
目前已知,该平台使用资源文件配置了如下网段:
- 192.168.42.2 - 192.168.42.254
- 192.168.43.2 - 192.168.43.254
- 192.168.44.2 - 192.168.44.254
- 192.168.45.2 - 192.168.45.254
- 192.168.46.2 - 192.168.46.254
- 192.168.47.2 - 192.168.47.254
- 192.168.48.2 - 192.168.48.254
- 192.168.49.2 - 192.168.49.254
- 192.168.50.2 - 192.168.50.254
- 192.168.51.2 - 192.168.51.254
而出现问题时,则是选择了192.168.42.2-254这一网段,导致转发到了USB,手机无法收到HDCP的OFFER报文;
此时,我们发现,作为桥接的网络接口(wifi_br0)实际分配的IP地址为192.168.43.x,因此理论上分配的IP段应为192.168.43.2-254;
因此我们对寻找匹配IP地址段的代码逻辑进行了跟踪,最后定位到了dhcp.c 中的complete_context() 函数中; 代码路径:external/dnsmasq/src/dhcp.c
static int complete_context(struct in_addr local, int if_index,
struct in_addr netmask, struct in_addr broadcast, void *vparam)
{
struct dhcp_context *context;
struct iface_param *param = vparam;
for (context = daemon->dhcp; context; context = context->next) {
if (!(context->flags & CONTEXT_NETMASK) &&
(is_same_net(local, context->start, netmask) ||
is_same_net(local, context->end, netmask))) {
if (context->netmask.s_addr != netmask.s_addr &&
!(is_same_net(local, context->start, netmask) &&
is_same_net(local, context->end, netmask))) {
strcpy(daemon->dhcp_buff, inet_ntoa(context->start));
strcpy(daemon->dhcp_buff2, inet_ntoa(context->end));
my_syslog(MS_DHCP | LOG_WARNING, _("DHCP range %s -- %s is not consistent with netmask %s"),
daemon->dhcp_buff, daemon->dhcp_buff2, inet_ntoa(netmask));
}
context->netmask = netmask;
}
if (context->netmask.s_addr) {
if (is_same_net(local, context->start, context->netmask) &&
is_same_net(local, context->end, context->netmask)) {
if (if_index == param->ind && context->current == context) {
context->router = local;
context->local = local;
context->current = param->current;
param->current = context;
}
...
} else if (param->relay.s_addr && is_same_net(param->relay, context->start, context->netmask)) {
...
}
}
}
return 1;
}
通过分析代码,可以得出如下结论:
- 这个函数中param->current有可能被重复赋值,返回结果以最后一次为准;
- 如果dhcp_context没有设置CONTEXT_NETMASK这个flag,则会采用从内核获取的属于同一网段的子网掩码作为dhcp_context的子网掩码;
再添加日志以后对比复现问题与正常情况的日志输出后,确认问题原因如下:
- 由于之前startTethering启动dnsmasq时没有指定每个dhcp-range的子网掩码,因此CONTEXT_NETMASK这个flag没有设置;
- 出现问题的WIFI配置的子网掩码为255.255.252.0;
- 遍历所有dhcp_context时,是从高网段(192.168.51.x)向低网段遍历(192.168.42.x)
- wifi_br0默认配置的IP地址为192.168.43.x
- 由于2的原因,在遍历每个dhcp_context时,192.168.43.2-254与192.168.42.2-254两个网段均被is_same_net函数判定为同一网段;
- 又由于3的原因,192.168.42.x会覆盖192.168.43.x,作为最终选择的IP网段;
因此会导致DHCP选择了192.168.42.2-254网段分配IP地址,又由于这一网段在iptables层面上是路由给USB的,从而导致热点的DHCP无法完成;
解决方案
从逻辑上来看,我个人认为热点在通过DHCP分配IP地址时,并不需要考虑连接WIFI的子网掩码,这里是不合理的; 因此我认为需要想办法将每个IP地址段对应的dhcp_context的flags中加上CONTEXT_NETMASK;
由于参数是system_server获取,并一层一层发送给netd,并由后者通过execv启动的dnsmasq,因此可以做如下尝试:
代码路径:system/netd/server/TetherController.cpp:
for (int addrIndex = 0; addrIndex < num_addrs; addrIndex += 2) {
argVector.push_back(
StringPrintf("--dhcp-range=%s,%s,255.255.255.0,1h",
dhcp_ranges[addrIndex], dhcp_ranges[addrIndex+1]));
}
这样的话,在dnsmasq.c的main函数中会对参数进行解析,大致步骤为:
- 使用逗号(“,”)拆分参数;
- 第一个参数为IP段起始地址;
- 第二个参数为IP段终止地址;
- 如果第三个参数包含句点(“.”),则将第三个参数解析为子网掩码,否则解析为租期;
- 如果第三个参数解析为子网掩码,且包含句点(“.”),则第四个参数解析为广播地址(broadcast),否则解析为租期;
- 则第四个参数解析为广播地址,则第五个参数解析为租期;
代码片段如下:
...
case 'F': {
int k, leasepos = 2;
char *cp, *a[5] = { NULL, NULL, NULL, NULL, NULL };
struct dhcp_context *new = opt_malloc(sizeof(struct dhcp_context));
new->next = daemon->dhcp;
new->lease_time = DEFLEASE;
new->addr_epoch = 0;
new->netmask.s_addr = 0;
new->broadcast.s_addr = 0;
new->router.s_addr = 0;
new->netid.net = NULL;
new->filter = NULL;
new->flags = 0;
gen_prob = _("bad dhcp-range");
if (!arg) {
option = '?';
break;
}
while(1) {
for (cp = arg; *cp; cp++)
if (!(*cp == ' ' || *cp == '.' || (*cp >='0' && *cp <= '9')))
break;
if (*cp != ',' && (comma = split(arg))) {
if (strstr(arg, "net:") == arg) {
struct dhcp_netid *tt = opt_malloc(sizeof (struct dhcp_netid));
tt->net = opt_string_alloc(arg+4);
tt->next = new->filter;
new->filter = tt;
} else {
if (new->netid.net)
problem = _("only one netid tag allowed");
else
new->netid.net = opt_string_alloc(arg);
}
arg = comma;
} else {
a[0] = arg;
break;
}
}
for (k = 1; k < 5; k++) {
if (!(a[k] = split(a[k-1]))) {
break;
}
}
if ((k < 2) || ((new->start.s_addr = inet_addr(a[0])) == (in_addr_t)-1)) {
option = '?';
} else if (strcmp(a[1], "static") == 0) {
new->end = new->start;
new->flags |= CONTEXT_STATIC;
} else if (strcmp(a[1], "proxy") == 0) {
new->end = new->start;
new->flags |= CONTEXT_PROXY;
} else if ((new->end.s_addr = inet_addr(a[1])) == (in_addr_t)-1) {
option = '?';
}
if (ntohl(new->start.s_addr) > ntohl(new->end.s_addr)) {
struct in_addr tmp = new->start;
new->start = new->end;
new->end = tmp;
}
if (option != '?' && k >= 3 && strchr(a[2], '.') &&
((new->netmask.s_addr = inet_addr(a[2])) != (in_addr_t)-1)) {
new->flags |= CONTEXT_NETMASK;
leasepos = 3;
if (!is_same_net(new->start, new->end, new->netmask))
problem = _("inconsistent DHCP range");
}
daemon->dhcp = new;
if (k >= 4 && strchr(a[3], '.') &&
((new->broadcast.s_addr = inet_addr(a[3])) != (in_addr_t)-1)) {
new->flags |= CONTEXT_BRDCAST;
leasepos = 4;
}
if (k >= leasepos+1) {
if (strcmp(a[leasepos], "infinite") == 0)
new->lease_time = 0xffffffff;
else {
int fac = 1;
if (strlen(a[leasepos]) > 0) {
switch (a[leasepos][strlen(a[leasepos]) - 1]){
case 'd':
case 'D':
fac *= 24;
case 'h':
case 'H':
fac *= 60;
case 'm':
case 'M':
fac *= 60;
case 's':
case 'S':
a[leasepos][strlen(a[leasepos]) - 1] = 0;
}
new->lease_time = atoi(a[leasepos]) * fac;
if (new->lease_time < 120)
new->lease_time = 120;
}
}
}
break;
...
如此一来,每个dhcp_context都会设置255.255.255.0为其子网掩码,不会再取WIFI的子网掩码卵用了; 当然,这里作为实例用,仅在netd中将所有IP段的子网掩码都硬编码为255.255.255.0,实际使用可以自行设计框架从上层读取配置文件并一层一层传入;
后记
问题解决了,具体编码还在考虑中,由于设计一套从上到下的框架并不容易被AOSP主线采纳。因此如果有AOSP贡献者能看到这篇文章,并将其在AOSP主线实现,当然是最好的结果了;
另外,由于本人才开始接触WIFI模块,因此这篇文章更多是基于分析这个问题的机会,梳理了下dnsmasq这一进程分配IP地址的逻辑;以及从诸多网段中选出合适的哪一个的逻辑;
文笔有限,仅供参考;
|