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;