udhcpd_main的Super loop
到这一步,DHCP服务器开始提供具体的服务,super loop主要包括建立socket监听及信号处理、获取并提取报文、根据state和报文内容做出响应。
建立Socket监听和signal处理器
若未建立本地socket监听或监听意外关闭,重新建立,若建立失败,则打印log并退出。本地监听地址端口:SERVER_PORT (67),监听硬件接口:server_config.interface,监听地址:INADDR_ANY(所有地址)
if (server_socket < 0) {
server_socket = listen_socket(/*INADDR_ANY,*/ SERVER_PORT,
server_config.interface);
}
使用select+FD模型,对socket和socketpair进行监控
/* 添加server_socket和signal_pipe进rfds集合*/
max_sock = udhcp_sp_fd_set(&rfds, server_socket);
/* int udhcp_sp_fd_set(fd_set *rfds, int extra_fd)
{
FD_ZERO(rfds);
FD_SET(signal_pipe[0], rfds);
if (extra_fd >= 0) {
fcntl(extra_fd, F_SETFD, FD_CLOEXEC);
FD_SET(extra_fd, rfds);
}
return signal_pipe[0] > extra_fd ? signal_pipe[0] : extra_fd;*/
/* 如果auto_time不为0,更新等待时间tv.tv_sec为auto_time的剩余时间 */
if (server_config.auto_time) {
tv.tv_sec = timeout_end - monotonic_sec();
tv.tv_usec = 0;
}
/* 如果auto_time为0,或tv_sec大于0时,建立select等待server_socket和signal_pipe的信号 */
if (!server_config.auto_time || tv.tv_sec > 0) {
max_sock = server_socket > signal_pipe[0] ? server_socket : signal_pipe[0];
/* 对两个fd都进行可读性检测 */
retval = select(max_sock + 1, &rfds, NULL, NULL,
/*如果auto_time不为0,则非阻塞,超时时间为上述的剩余时间;如果为0,则time设为NULL,select将一直阻塞直到某个fd上接收到信号*/
server_config.auto_time ? &tv : NULL);
} else retval = 0; /* If we already timed out, fall through */
/* 若直到超时都没有接收到信号,则立即写lease文件,并更新time_end */
if (retval == 0) {
write_leases();
timeout_end = monotonic_sec() + server_config.auto_time;
continue;
}
if (retval < 0 && errno != EINTR) {
DEBUG("error on select");
continue;
}
/* 若signal_pipe接收到可读signal(signal_handler将signal写入signal_pipe[1],根据socketpair的特性,此时signal_pipe[0]将可读,产生一个可读的信号) */
switch (udhcp_sp_read(&rfds)) {
/* 接收到SIGUSR1,立即写leases,并更新time_end */
case SIGUSR1:
bb_info_msg("Received a SIGUSR1");
write_leases();
/* why not just reset the timeout, eh */
timeout_end = monotonic_sec() + server_config.auto_time;
continue;
case SIGTERM:
bb_info_msg("Received a SIGTERM");
goto ret0;
case 0: break; /* no signal */
default: continue; /* signal or error (probably EINTR) */
}
这一步的主要目的就是对server_socket和socketpair建立监听,并根据对socketpair的信号情况及leases结构的内容,执行write_leases函数,该函数将最新的leases结构体里的内容写入lease_file,根据yiaddr、remaining和当前时间来更新lease_time,每次执行完write_leases函数和,都有更新time_end时刻,write_leases定义于files.c中。
获取和提取报文
调用udhcp_get_packet函数从server_socket接收数据报文填充到packet,注意该函数在调用read读取报文之后,会对报文内的数据进行简单校验,包括cookie(若不为DHCP_MAGIC,则丢弃),以及根据op和options的vender字段判断是否强制设定flag字段为1(广播)。
/* this waits for a packet – idle */
bytes = udhcp_get_packet(&packet, server_socket); /* this waits for a packet - idle */
if (bytes < 0) {
if (bytes == -1 && errno != EINTR) {
DEBUG("error on read, %s, reopening socket", strerror(errno));
close(server_socket);
server_socket = -1;
}
continue;
}
使用get_option函数提取state状态信息,gei_option函数是从packet的options字段中,基于CLV(1+1+n)格式,提取出指定的选项,返回该选项的value值对应的指针。
state = get_option(&packet, DHCP_MESSAGE_TYPE);
if (state == NULL) {
bb_error_msg("cannot get option from packet, ignoring");
continue;
}
寻找静态IP,首选根据packet的chaddr,在server_config.static_leases列表中查找静态IP,返回IP值及其在列表中的索引地址,并将本地leases的信息更新为该静态IP,静态IP的expires为0,;若没有找到,则根据mac在本地lease列表中查找返回lease。
/* Look for a static lease */
static_lease_ip = getIpByMac(server_config.static_leases, &packet.chaddr);
if (static_lease_ip) {
bb_info_msg("Found static lease: %x", static_lease_ip);
memcpy(&static_lease.chaddr, &packet.chaddr, 16);
static_lease.yiaddr = static_lease_ip;
static_lease.expires = 0;
lease = &static_lease;
} else {
lease = find_lease_by_chaddr(packet.chaddr);
}
根据state和packet内容响应报文
根据RFC2131,DHCP服务器接收到且需要处理的消息只有5种,即DHCPDISCOVER、DHCPREQUEST、DHCPDECLINE、DHCPRELEASE和DHCPINFORM,都存储在state中。根据RFC2131定义的客户端状态转移图,对于以上五种消息,根据packet内容的不同,也有不同的响应。
DHCPDISCOVER
因为服务器只有在DHCP客户端处于初始化的时候,INIT_SELECTING状态,才会接收到DHCPDISCOVER消息,因此直接回复DHCPOFFER消息,根据本地lease列表和报文中MAC地址的情况,
case DHCPDISCOVER:
DEBUG("Received DISCOVER");
if (sendOffer(&packet) < 0) {
bb_error_msg("send OFFER failed");
}
break;
sendOffer函数基于接收的packet发送DHCPOFFER消息,对于old packet的处理,除了在init_packet中对发送消息的IP和MAC地址等的修改外,还需要根据发送DISCOVER消息的客户端的MAC地址,查询本地leases,是否是静态IP,是否是已分配的IP,是否是保留IP,是否是新请求的IP等条件,来配置yiaddr字段。
/* send a DHCP OFFER to a DHCP DISCOVER */
int sendOffer(struct dhcpMessage *oldpacket)
{
struct dhcpMessage packet;
struct dhcpOfferedAddr *lease = NULL;
u_int32_t req_align, lease_time_align = server_config.lease;
unsigned char *req, *lease_time;
struct option_set *curr;
struct in_addr addr;
uint32_t static_lease_ip;
init_packet(&packet, oldpacket, DHCPOFFER);
/* 配置yiaddr字段 */
/* ADDME: if static, short circuit */
static_lease_ip = getIpByMac(server_config.static_leases, oldpacket->chaddr, NULL);
if( !static_lease_ip )
{
/* the client is in our lease/offered table
* and this ip in table is not reserve to another static lease
*/
if ((lease = find_lease_by_chaddr(oldpacket->chaddr)) &&
check_ip_reserve_another(lease->yiaddr, oldpacket->chaddr) == 0) {
if (!lease_expired(lease))
lease_time_align = lease->expires - time(0);
packet.yiaddr = lease->yiaddr;
/* Or the client has a requested ip */
} else if ((req = get_option(oldpacket, DHCP_REQUESTED_IP)) &&
/* Don't look here (ugly hackish thing to do) */
memcpy(&req_align, req, 4) &&
/* and the ip is in the lease range */
ntohl(req_align) >= ntohl(server_config.start) &&
ntohl(req_align) <= ntohl(server_config.end) &&
/* and its not already taken/offered */ /* ADDME: check that its not a static lease */
((!(lease = find_lease_by_yiaddr(req_align)) ||
/* or its taken, but expired */ /* ADDME: or maybe in here */
lease_expired(lease))) &&
/* check to see if this ip
* is reserved as another mac
*/
check_ip_reserve_another(req_align, oldpacket->chaddr) == 0) {
packet.yiaddr = req_align; /* FIXME: oh my, is there a host using this IP? */
/* otherwise, find a free IP */ /*ADDME: is it a static lease? */
} else {
packet.yiaddr = find_address(0);
/* try for an expired lease */
if (!packet.yiaddr) packet.yiaddr = find_address(1);
}
if(!packet.yiaddr) {
LOG(LOG_WARNING, "no IP addresses to give -- OFFER abandoned");
return -1;
}
/* add a lease into the table, clearing out any old ones */
if (!add_lease(packet.chaddr, packet.yiaddr, server_config.offer_time)) {
LOG(LOG_WARNING, "lease pool is full -- OFFER abandoned");
return -1;
}
/* 若oldpacket的options字段有lease_time元素,即client请求了一个指定的超时时间,则获取该值,并将server_config.lease更新为二者的较大值 */
if ((lease_time = get_option(oldpacket, DHCP_LEASE_TIME))) {
memcpy(&lease_time_align, lease_time, 4);
lease_time_align = ntohl(lease_time_align);
if (lease_time_align > server_config.lease)
lease_time_align = server_config.lease;
}
/* Make sure we aren't just using the lease time from the previous offer */
if (lease_time_align < server_config.min_lease)
lease_time_align = server_config.lease;
/* ADDME: end of short circuit */
} else {
/* It is a static lease... use it */
packet.yiaddr = static_lease_ip;
}
/* 在新的packet中添加lease的内容 */
add_simple_option(packet.options, DHCP_LEASE_TIME, htonl(lease_time_align));
/* 添加其他的options内容到packet的options字段 */
curr = server_config.options;
while (curr) {
if (curr->data[OPT_CODE] != DHCP_LEASE_TIME)
add_option_string(packet.options, curr->data);
curr = curr->next;
}
/* 添加bootp选项,包括sname和boot_file字段 */
add_bootp_options(&packet);
addr.s_addr = packet.yiaddr;
LOG(LOG_INFO, "sending OFFER of %s", inet_ntoa(addr));
return send_packet(&packet, 0);
}
DHCPREQUEST
服务器在接收到REQUEST消息时的处理比较复杂,因为客户端可以在多种情况下发送DHCPREQUEST消息,比如SELECTING状态、INIT_REBOOTING状态、RENEWING or REBINDING状态,三类状态在options字段有明显的区别,服务器对其处理如下:
case DHCPREQUEST:
DEBUG(LOG_INFO, "received REQUEST");
requested = get_option(&packet, DHCP_REQUESTED_IP);
server_id = get_option(&packet, DHCP_SERVER_ID);
if (requested) memcpy(&requested_align, requested, 4);
if (server_id) memcpy(&server_id_align, server_id, 4);
/* lease不为NULL,即在本地有该client的记录。即便是从INIT过来的client,在接收到它的DHCPDISCOVER并发送OFFER之后,也在本地做了记录 */
if (lease) { /*ADDME: or static lease */
/* DHCPREQUEST中,server_id若不为0,则是SELECTING State(该选项在client从DISCOVER,到收到OFFER后,回复的REQUEST中必须包含,指定选中的server,在其他情况下不需要包含(详见RFC2131表5))*/
if (server_id) {
DEBUG(LOG_INFO, "server_id = %08x", ntohl(server_id_align));
/* SELECTING状态下,校验server_id和本地server匹配,request_ip存在且和lease分配的yiaddr一致,则发送ACK */
if (server_id_align == server_config.server && requested &&
requested_align == lease->yiaddr) {
sendACK(&packet, lease->yiaddr);
}
}
/* server_id为0,是其他状态, */
else {
reserved_another = check_ip_reserve_another(lease->yiaddr, packet.chaddr);
/* 根据RFC2131,DHCPRERQUEST报文中的request_ip字段MUST (in SELECTING or INIT-REBOOT) MUST NOT (in BOUND or RENEWING) */
if (requested) {
/* INIT-REBOOT State,
/*如果请求的IP和分配的IP一致,且没有被分配给其他主机, 发送ACK,否则发送NAK */
if (lease->yiaddr == requested_align)
sendACK(&packet, lease->yiaddr);
else sendNAK(&packet);
} else {
/* RENEWING or REBINDING State,如果分配的yiaddr和客户端IP一致,即请求RENEWING或REBINDING的主机确实和本地有关联,则继续,进一步校验IP是否已被分配 */
if (requested) {
/* INIT-REBOOT State */
if (lease->yiaddr == requested_align)
sendACK(&packet, lease->yiaddr);
else
sendNAK(&packet);
} else if (lease->yiaddr == packet.ciaddr) {
/* RENEWING or REBINDING State */
sendACK(&packet, lease->yiaddr);
} else {
/* don't know what to do!!!! */
sendNAK(&packet);
}
/* what to do if we have no record of the client,一个没有任何记录却收到其REQUEST消息的client,可以认为是一个错误或者恶意攻击,一般对其进行静默处理或者设置黑户 */
} else if (server_id) {
/* 若server_id不为0,则处于SELECTING State,则该REQUEST消息可能是来自其他未收到其广播的DISCOVER消息的client,或意外接收到其他网段的消息,采取静默处理 */
} else if (requested) {
/* 若携带request_ip,则处于INIT-REBOOT State,若该IP在本地lease列表中存在,且lease已超时,则丢弃该lease,否则直接发送NAK */
lease = find_lease_by_yiaddr(requested_align);
if (lease) {
if (lease_expired(lease)) {
/* probably best if we drop this lease */
memset(lease->chaddr, 0, 16);
/* make some contention for this address */
} else
sendNAK(&packet);
} else {
uint32_t r = ntohl(requested_align);
if (r < server_config.start_ip
|| r > server_config.end_ip
) {
sendNAK(&packet);
}
/* else remain silent */
}
} else {
/* RENEWING or REBINDING State */
}
break;
2.3.3.3 DHCPDECLINE
收到DHCPDECLINE消息,则丢弃本地记录的lease:设置chaddr为0,即清除对该client的记录,并且设置该lease在decline_time后
case DHCPDECLINE:
DEBUG("Received DECLINE");
if (lease) {
memset(lease->chaddr, 0, 16);
lease->expires = time(0) + server_config.decline_time;
}
break;
2.3.3.4 DHCPRELEASE
服务器在接收到客户端DHCPRELEASE消息后,直接设置本地lease超时,但是并未清除该client的记录,即下一次该客户端可以跳过DISCOVER,直接获取IP配置。
case DHCPRELEASE:
DEBUG(LOG_INFO,"received RELEASE");
if (lease) lease->expires = time(0);
break;
2.3.3.5 DHCPINFORM
Client to server, asking only for local configuration parameters; client already has externally configured network address.
客户端在已经有外部配置网络地址时,发送DHCPINFORM只为了获取本地配置参数。服务器接收后,直接发送INFORM数据包、
case DHCPINFORM:
DEBUG("Received INFORM");
send_inform(&packet);
break;