分类: 系统运维
2013-11-28 13:34:32
原文地址:ISC DHCP 分配IP流程分析 作者:GFree_Wind
在Linux的世界中,ISC DHCP被广泛应用。因为ISC DHCP的版权是允许在保留其版权声明的基础上,使用,复制,修改其源代码。对于Linux开发人员来说,在掌握了ISC DHCP分配IP的流程和主要机制后,可以很容易的添加自己的功能。本文中的代码来自 ISC DHCP 4.2版本。
在阅读本文之前,最好对ISC DHCP的配置文件中的各种元素有所了解。因为在其源代码中的关键变量名字都是与配置文件中的名字相同或相似,掌握了DHCP的配置,有助于理解其源代码。
DHCP简介
DHCP是动态主机配置协议(Dynamic Host Configuration Protocol)的缩写。它的流程比较简单,一般情况下只需要4个包(DHCPDISCOVER, DHCPOFFER, DHCPREQUEST, DHCPACK)的交互,就可以完成IP的分配。
当client第一次连到网络时,它会发现自己没有任何的网络设置包括IP地址。于是client会向整个网络广播DHCPDISCOVER包,申请IP以及网络设置。网络中的DHCP server在收到DHCPDISCOVER包后,根据自己的配置挑选一个IP以及相应的网络设置通过DHCPOFFER包发送给client。当client收到DHCPOFFER后,它会选择是否接受这个offer。如果选择接受的话,client会发送DHCPREQUEST到server。server在接收到DHCPREQUEST,会确认这个IP lease成立。到此,申请一个新的IP的DHCP过程结束。当然,在正常的网络环境中,整个过程有可能不是这么简单。例如网络中有多个DHCP服务器,或者该IP已经被其他client占用等情况。DHCP协议中对这些情况都有相应的处理,在此就不一一介绍了。
在获得IP以后,client需要在lease的1/2左右的时间发送DHCPREQUEST延长lease。如果得到了server的DHCPACK的确认回复,那么lease就继续延长。如果没有得到DHCPACK的回复,client仍然可以使用该IP,但是需要在lease的7/8时间发送DHCPDISCOVER重复申请IP的过程。如果失败的话,那么就要立刻放弃该IP。
IP的回收主要是通过两种途径。一是当client不需要这个IP时(如关机),通过发送DHCPRELEASE包给server来结束lease;其次是server会定期检查已分配的IP lease,将过期lease的IP回收。
ISC DHCP 分配IP流程分析
下文将通过对ISC DHCP的主要源代码的分析,来解析其分配IP的流程和机制。在源代码中其主要函数dhcpdiscover、dhcprequest、和dhcprelease分别用于处理与其函数名字相同的包。
确定分配IP的网段函数void dhcp (struct packet *packet)是处理DHCP包的入口函数,在进入这个函数的时候,对包的解析已经完成,已经将包的内容转换成内部结构struct packet。后文所有分析也都是建立在这个基础之上的。
一进入dhcp函数,首先就是调用int locate_network (struct packet *packet)来确定client所属subnet,以便从这个subnet下分配正确的IP。
清单1. 查找link selection和subnet selection optionsif ((oc = lookup_option(&agent_universe, packet->options,
RAI_LINK_SELECT)) == NULL)
oc = lookup_option(&dhcp_universe, packet->options,
DHO_SUBNET_SELECTION);
首先,先查找DHCP包中是否有relay-agent-option下link selection sub-option,这个option用于当client与server处于不同网段,需要DHCP relay转发时,client想要得到一个relay agent不同网段的IP。具体信息请参考RFC 3527。如果没有该option,就继续在包中查找subnet selection option。这个option用于在client与server同一网段的情况下,client需要获得不同网段的IP,具体信息参考RFC3011。这两个option在功能上有相似之处,都是client要求获得一个指定网段的IP。不同之处在于,relay-agent-option是用于DHCP relay的,也就说DHCP包是通过relay转发到server端的,client与server处于不同的物理网段。而subnet selection option,是client与server处于同一个物理网段,但是想要获得不同网段的IP,而由client将这个option加入到DHCP包中。
清单2. 使用网卡所在网段为预分配的subnetif (!oc && !packet -> raw -> giaddr.s_addr) {
if (packet -> interface -> shared_network) {
shared_network_reference
(&packet -> shared_network,
packet -> interface -> shared_network, MDL);
return 1;
}
return 0;
}
如果没有找到这两个option,那么通过packet -> raw -> giaddr.s_addr来判断这个包是否是由relay转发过来的。packet -> raw -> giaddr.s_addr是relay的地址,由relay填写。如果不是转发过来的包,那么到此就可以确定client与server同一网段,并且没有要求不同网段的IP。那么就确定了接收到该包网卡所属的网段,为分配IP的网段,并赋给packet->shared_network。
清单3. 使用option或relay地址确定subnet
if (oc) {
memset (&data, 0, sizeof data);
if (!evaluate_option_cache (&data, packet, (struct lease *)0,
(struct client_state *)0,
packet -> options,
(struct option_state *)0,
&global_scope, oc, MDL)) {
return 0;
}
if (data.len != 4) {
return 0;
}
ia.len = 4;
memcpy (ia.iabuf, data.data, 4);
data_string_forget (&data, MDL);
} else {
ia.len = 4;
memcpy (ia.iabuf, &packet -> raw -> giaddr, 4);
}
if (find_subnet (&subnet, ia, MDL)) {
shared_network_reference (&packet -> shared_network,
subnet -> shared_network, MDL);
subnet_dereference (&subnet, MDL);
return 1;
}
在没有前面两个option的情况下,server将通过packet -> raw -> giaddr来确定分配IP的subnet。
通过对locate_network的分析,我们可以确定了subnet选择因素的优先级为link selection sub-option>subnet selection option>relay address>网卡的subnet。
确定要分配的IP
函数dhcpdiscover通过调用find_lease来确定分配哪个IP给client。
首先sever会尝试用DHCP请求中的client identifer或者hardware来确定该请求是否来自于一个配置文件指定的host。(通常当client的管理员没有指定client indentifier的时候,client会把自己的网卡的MAC地址作为client identifer发给server。)
通过find_hosts_by_uid(&hp, client_identifier.data, client_identifier.len, MDL)来使用client identifier找到host即hp,通过find_hosts_by_haddr(&hp, packet->raw->htype, packet->raw->chaddr, packet->raw->hlen, MDL)来使用MAC地址来找到host。找到host后,通过host尝试查找fixed lease——即在配置文件的host中,指定的IP地址。这是通过mockup_lease (&fixed_lease, packet, share, host)这个函数实现的。这里注意的是,传入参数share为预分配IP的subnet。将它作为参数,是为了排除不在该subnet中的fixed ip,即使这个IP是host的指定IP。
其实除了使用client indentifer和MAC地址来查找host的方式外,还支持通过指定option来查找host,但是这种用法并不常见,在此就不说明了。通过host确定的IP,在ISC DHCP中被称为fixed IP,其对应的lease自然被称为fixed lease。
清单4. 通过client identifier获得fixed lease
if (find_hosts_by_uid (&hp, client_identifier.data,
client_identifier.len, MDL)) {
/* Remember if we know of this client. */
packet -> known = 1;
mockup_lease (&fixed_lease, packet, share, hp);
}
在通过host来确定要分配IP的同时,也会使用函数find_lease_by_uid(&uid_lease, client_identifer.data, client_identifier.len, MDL)和find_lease_by_hw_addr(&hw_lease, h.hbuf, h.hlen, MDL)来直接通过client_identifier和MAC地址确定lease。在代码中,有两个lease的hash表,分别以client identifier和hardware address为key。从这里我们可以看出,DHCP server一般情况下,都是会给client上次同样的IP。
client除了被动的接受server给分配的IP,它还可以主动向server提出要求,要求想要得到的IP。这是通过client在DHCP包中的client地址,或者使用requested address option来实现的。
清单5. 获得client的请求地址
if (packet -> raw -> ciaddr.s_addr) {
cip.len = 4;
memcpy (cip.iabuf, &packet -> raw -> ciaddr, 4);
} else {
/* Look up the requested address. */
oc = lookup_option (&dhcp_universe, packet -> options,
DHO_DHCP_REQUESTED_ADDRESS);
memset (&d1, 0, sizeof d1);
if (oc &&
evaluate_option_cache (&d1, packet, (struct lease *)0,
(struct client_state *)0,
packet -> options,
(struct option_state *)0,
&global_scope, oc, MDL)) {
packet -> got_requested_address = 1;
cip.len = 4;
memcpy (cip.iabuf, d1.data, cip.len);
data_string_forget (&d1, MDL);
} else
cip.len = 0;
}
server会使用这个cip来分配一个lease,然后再通过一系列检查来判断是否可以使用这个lease。在不与其他配置的不冲突的情况下,尽量满足client的要求,分配给其要求的IP。
确定分配IP的优先级是fixed lease>uid lease> hw lease > ip lease,其中 fixed lease是配置文件中host所配置的lease,uid lease 是通过uid确定的lease,hw lease是通过硬件地址查找到的lease,ip lease是client希望请求得到的lease。
处理DHCP包的函数调用过程
在解析完DHCP,将其转换为内部结构structpacket后,整个函数调用过程如下:
1. 入口函数dhcp;
2. 调用locate_network,确定从哪个subnet分配IP;
3. 根据DHCP包的类型调用dhcp_discover, dhcp_request或者其他相应的函数;
4. 在dhcp_discover/dhcp_request中,调用find_lease确定分配哪个ip lease;
5. 调用ack_lease构造reply包;
6. 调用supersede_lease将分配的lease添加到对应的hash中,并写入到dhcp server的数据库文件中;
7. 调用dhcp_reply将包发送给client;
由于篇幅所限,在此只简单的说明函数的调用过程和用途,但是比较关键的部分就是locate_network和find_lease这两个函数,已经在前文对它们进行了比较详细的分析。
如何处理过期lease
当client不需要该IP时,client会发送DHCPRELEASE来通知server,这个过程比较简单。即使serer收不到DHCPRELEASE,也会在lease到期时,收回该lease。
在ISC DHCP的源代码中,维护了一个timer_list。该timer_list是一个单链表,最早的timer排在第一位。当server调用select去等待DHCP包时所用的超时时间,就是第一个timer的超时时间。
处理过期lease的函数为pool_timer。它会循环遍历active lease链表,当发现过期lease时,调用supersede_lease将lease重新插入到合适的lease table中,以及写入server的数据库文件,完成的过期lease的收回。当遇到第一个没有过期的lease时,就会退出循环,并根据这个lease重新计算新的time out时间,并加入到timer list中。