udhcp源码剖析(四)——DHCP服务器的superloop

  • Post author:
  • Post category:其他


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;



版权声明:本文为who538592原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。