udhcp源碼剖析(四)——DHCP服務器的superloop

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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章