[Android][dnsmasq]从一个bug入门dnsmasq的IP地址分配逻辑

背景

今天接到做WIFI以后的第一个问题分析,现象如下:

复现步骤:

  • 机器连接某个特定WIFI (办公场所提供,无法登陆后台确认设置信息)
  • 机器再打开热点(即桥接模式)
  • 手机连接热点

期望结果:

  • 手机可以正常连接

实际结果:

  • 手机无法连接,始终停留在“正在获取IP地址”;

这个问题比较“神奇”的地方在于:

  1. 只有当机器连接某个特定WIFI时出现问题;
  2. 连接该特定WIFI时稳定复现;
  3. 连接其他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

    // USB is  192.168.42.1 and 255.255.255.0
    // Wifi is 192.168.43.1 and 255.255.255.0
    // BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1
    // with 255.255.255.0
    // P2P is 192.168.49.1 and 255.255.255.0
    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

/*
 * 这个函数是作为指针,在调用iface_enumerate函数是作为参数传入,并待后者从内核获取到所有网络接口后通过回调执行:
 * !iface_enumerate(&parm, complete_context, NULL)
 * 注意,如下代码对缩进与部分不影响理解的逻辑进行过调整,仅供此次分析使用:
 */
static int complete_context(struct in_addr local, int if_index, 
			    struct in_addr netmask, struct in_addr broadcast, void *vparam)
{
	//每一个dhcp_context 结构体表示一个DHCP可分配的上下文,包括其中就包括IP网段,因此可以大致理解为一个IP地址段就是一个dhcp_context,这虽然不太严谨,但是不影响此处分析;
    struct dhcp_context *context;
    struct iface_param *param = vparam;

	//遍历所有dhcp_context,并获取每个的IP地址段与子网掩码
    for (context = daemon->dhcp; context; context = context->next) {
    	//如果上下文的flags中不包含CONTEXT_NETMASK,且该ip地址(local)属于该dhcp_context的IP地址段,
    	//则将从内核获取的netmask赋值给上下文的netmask变量;
        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;
        }

		//如果上下文中的netmask地址不为0
        if (context->netmask.s_addr) {
        	//且该ip地址(local)属于该dhcp_context的IP地址段,则将其dhcp_context赋值给context->current,
        	//即表征最终选择的dhcp_context;
            if (is_same_net(local, context->start, context->netmask) &&
            		is_same_net(local, context->end, context->netmask)) {
                /* link it onto the current chain if we've not seen it before */
                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;
}

通过分析代码,可以得出如下结论:

  1. 这个函数中param->current有可能被重复赋值,返回结果以最后一次为准;
  2. 如果dhcp_context没有设置CONTEXT_NETMASK这个flag,则会采用从内核获取的属于同一网段的子网掩码作为dhcp_context的子网掩码;

再添加日志以后对比复现问题与正常情况的日志输出后,确认问题原因如下:

  1. 由于之前startTethering启动dnsmasq时没有指定每个dhcp-range的子网掩码,因此CONTEXT_NETMASK这个flag没有设置;
  2. 出现问题的WIFI配置的子网掩码为255.255.252.0;
  3. 遍历所有dhcp_context时,是从高网段(192.168.51.x)向低网段遍历(192.168.42.x)
  4. wifi_br0默认配置的IP地址为192.168.43.x
  5. 由于2的原因,在遍历每个dhcp_context时,192.168.43.2-254与192.168.42.2-254两个网段均被is_same_net函数判定为同一网段;
  6. 又由于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(
                    //Original Code:
                    //StringPrintf("--dhcp-range=%s,%s,1h",
                    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':  /* --dhcp-range */ {
		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;
						/* fall though */
					      case 'h':
					      case 'H':
						fac *= 60;
						/* fall through */
					      case 'm':
					      case 'M':
						fac *= 60;
						/* fall through */
					      case 's':
					      case 'S':
						a[leasepos][strlen(a[leasepos]) - 1] = 0;
					}
		    
                    new->lease_time = atoi(a[leasepos]) * fac;
                    /* Leases of a minute or less confuse
                        some clients, notably Apple's */
                    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地址的逻辑;以及从诸多网段中选出合适的哪一个的逻辑;

文笔有限,仅供参考;

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值