udhcpd源码分析4--获取client报文及发包动作

1:重要的结构体

  获取的报文是UDP的payload部分,结构体struct dhcpMessage描述了dhcp报文的结构。

/* packet.h */

struct dhcpMessage {
    u_int8_t op;             /* 1 for client,2 for server */
    u_int8_t htype;          /* Ethernet Type (0x01)*/
    u_int8_t hlen;           /* Ethernet Len(6) */
    u_int8_t hops;           /* 若封包需要router传输,每经过一条加1,同一网段下为0 */
    u_int32_t xid;           /* transaction ID 客户端产生的事务ID用来标识一次DHCP C/S交互,dhcpc一旦运行这个值就是固定了表示客户端自己*/
    u_int16_t secs;          /* 客户端启动耗时(一般为0) */
    u_int16_t flags;         /* 0-15 bit 最低bit为1则server将以广播形式发包给client,其它未使用 */
    u_int32_t ciaddr;        /* 若client想继续使用之前获得的IP则填充在这(一般是client 的Inform包会填写) */
    u_int32_t yiaddr;        /* server回复client你可使用的IP(ACK,offer报文中填写) */
    u_int32_t siaddr;        /* 若client需要通过网络开机,从server发出的报文这里应该填写开机程序代码
                               所在的server地址 */
    u_int32_t giaddr;        /* 若需要跨网域进行DHCP发包,这里填写server发包的目的地址
                               (如果没有server一般是发给租赁出去的IP地址) */
    u_int8_t chaddr[16];     /* client的硬件地址 */
    u_int8_t sname[64];      /* server 的主机名 */
    u_int8_t file[128];      /* 若client需要通过网络开机,这里将填写开机程序名称,让后以TFTP传输 */
    u_int32_t cookie;        /* should be 0x63825363 */
    u_int8_t options[308];   /* 312 - cookie */ 
};

2:udhcpd收发包主干逻辑

  2.1 获得套接字接口函数listen_socket

/* socket.c */

int listen_socket(unsigned int ip, int port, char *inf)
{
    struct ifreq interface;
    int fd;
    struct sockaddr_in addr;
    int n = 1;

    DEBUG(LOG_INFO, "Opening listen socket on 0x%08x:%d %s
", ip, port, inf);
    /* 
        此套接字是IPPROTO_UDP类型,所以收到的包的内容就是UDP报文的payload数据
    */
    if ((fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {//PF --> protocol family
        DEBUG(LOG_ERR, "socket call failed: %s", strerror(errno));
        return -1;
    }
    
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;//AF --> Address family
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = ip;

    /* 
        地址重用,服务器程序停止后想立即重启,而新套接字可以马上使用同一端口(一般一个端口释放后两分钟
        之后才可以被使用)
    */
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *) &n, sizeof(n)) == -1) {
        close(fd);
        return -1;
    }
    
    /*
      允许此socket发送广播包,我的想法是,只要目的地址设成全255,这样默认就发送广播报了,这个选项作用
      体现在哪里呢?这是为了防止你误发广播包,虽然你的目的IP是255.255.255.255,但你没有设置这个选项
      发包时会返回EACCESS错误提醒
    */
    if (setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (char *) &n, sizeof(n)) == -1) {
        close(fd);
        return -1;
    }

    /* 
      将套接字绑定到特定的interface,此socket只接收到此interface的报文,socket发送的
      报文也只从此interface出去
    */
    strncpy(interface.ifr_ifrn.ifrn_name, inf, IFNAMSIZ);
    if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE,(char *)&interface, sizeof(interface)) < 0) {
        close(fd);
        return -1;
    }

    /*
      绑定地址结构(ip and port)到socket
    */
    if (bind(fd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) == -1) {
        close(fd);
        return -1;
    }
    
    return fd;
}

  函数listen_socket返回一个UDP套接字接口,此套接字是作为dhcp服务器端套接字,绑定的端口和interface分别是SERVER_PORT(67)和server_config.interface,所以此套接字只监听来自server_config.interface接口且端口是67的报文。

  2.2 获取报文函数get_packet

/* packet.c */

/* read a packet from socket fd, return -1 on read error, -2 on packet error */
int get_packet(struct dhcpMessage *packet, int fd)
{
    int bytes;
    int i;
    const char broken_vendors[][8] = {
        "MSFT 98",
        ""
    };
    char unsigned *vendor;

    memset(packet, 0, sizeof(struct dhcpMessage));
    bytes = read(fd, packet, sizeof(struct dhcpMessage));
    if (bytes < 0) {
        DEBUG(LOG_INFO, "couldn't read on listening socket, ignoring");
        return -1;
    }

    /* packet->cookie(Default:0x63825363)字段丢掉假冒的DHCP client报文 */
    if (ntohl(packet->cookie) != DHCP_MAGIC) {
        LOG(LOG_ERR, "received bogus message, ignoring");
        return -2;
    }
    DEBUG(LOG_INFO, "Received a packet");
    
    if (packet->op == BOOTREQUEST && (vendor = get_option(packet, DHCP_VENDOR))) {
        for (i = 0; broken_vendors[i][0]; i++) {
            if (vendor[OPT_LEN - 2] == (unsigned char) strlen(broken_vendors[i]) &&
                !strncmp(vendor, broken_vendors[i], vendor[OPT_LEN - 2])) {
                    DEBUG(LOG_INFO, "broken client (%s), forcing broadcast",
                        broken_vendors[i]);
                    packet->flags |= htons(BROADCAST_FLAG);
            }
        }
    }
                    

    return bytes;
}

  报文获取到之后是保存在struct dhcpMessage结构体中,结构体中的options成员是一个大数组,里面保存了许多可用的信息,这些信息都是以CLV的格式保存在一段连续的内存中的,服务器后续的动作需要依赖options中的某些值,如何有效的查询这段内存的某些值是很重要的,所以options.c文件里的部分函数就是专门来处理options成员数据的。

  2.3 处理options成员的相关函数

/* options.c */

获取options成员函数get_option:

/*
    get_option根据选项值(code)获得指向此选项内容的指针
    options字段在dhcp报文中是可选并且大小不定,这里定义的大小是308字节.所有的options都定义在这个308字节的
    数组里,如何组织各选项的结构很重要,dhcp报文的一般options字段里的内容依照CLV(code + length + value)的
    格式组织,特殊的如code=DHCP_PADDING<填充字节读到此code直接跳过>,DHCP_OPTION_OVER及DHCP_END<options结
    束标志>有各自不同的组织方式.
    options[308] 内容大概结构:

      byte    byte      length*byte         byte        byte    byte    length*byte
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    |      |        |                  |              |      |        |
    |code1 | length |    value         | DHCP_PADDING |code2 | length |   value
    |      |        |                  |              |      |        |
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -   
*/

/* get an option with bounds checking (warning, not aligned). */
unsigned char *get_option(struct dhcpMessage *packet, int code)
{
    int i, length;
    unsigned char *optionptr;
    int over = 0, done = 0, curr = OPTION_FIELD;
    
    optionptr = packet->options;
    i = 0;
    length = 308;
    while (!done) {
        if (i >= length) {
            LOG(LOG_WARNING, "bogus packet, option fields too long.");
            return NULL;
        }
        /* 检查code值是否匹配 */
        if (optionptr[i + OPT_CODE] == code) {
            if (i + 1 + optionptr[i + OPT_LEN] >= length) {
                LOG(LOG_WARNING, "bogus packet, option fields too long.");
                return NULL;
            }
            return optionptr + i + 2;
        }        

        /* 处理选项中特殊字段,DHCP_PADDING(跳过), DHCP_END(结束),DHCP_OPTION_OVER(自定义)*/    
        switch (optionptr[i + OPT_CODE]) {
        case DHCP_PADDING:
            i++;
            break;
        case DHCP_OPTION_OVER:
            if (i + 1 + optionptr[i + OPT_LEN] >= length) {
                LOG(LOG_WARNING, "bogus packet, option fields too long.");
                return NULL;
            }
            over = optionptr[i + 3];
            i += optionptr[OPT_LEN] + 2;
            break;
        case DHCP_END:
            if (curr == OPTION_FIELD && over & FILE_FIELD) {
                optionptr = packet->file;
                i = 0;
                length = 128;
                curr = FILE_FIELD;
            } else if (curr == FILE_FIELD && over & SNAME_FIELD) {
                optionptr = packet->sname;
                i = 0;
                length = 64;
                curr = SNAME_FIELD;
            } else done = 1;
            break;
        default:
            i += optionptr[OPT_LEN + i] + 2;//指针指向下一个选项值的code字段
        }
    }
    return NULL;
}

获取options中end的位置end_option:

/* return the position of the 'end' option (no bounds checking) */
/* 返回从optionptr到'end'之间的步长 optionptr必须是packet->options(就是指向options数组的头部) */
int end_option(unsigned char *optionptr) 
{
    int i = 0;
    
    while (optionptr[i] != DHCP_END) {
        if (optionptr[i] == DHCP_PADDING) i++;
        else i += optionptr[i + OPT_LEN] + 2;
    }
    return i;
}

添加一个选项内容string(string已经组织为CLV的结构)到options数组中add_option_string 

/* add an option string to the options (an option string contains an option code,
 * length, then data) */
/* 添加一个选项到optionptr指向的options数组中,optionptr必须指向此数组的头部!*/
int add_option_string(unsigned char *optionptr, unsigned char *string)
{
    int end = end_option(optionptr);
    
    /* end position + string length + option code/length + end option */
    if (end + string[OPT_LEN] + 2 + 1 >= 308) {
        LOG(LOG_ERR, "Option 0x%02x did not fit into the packet!", string[OPT_CODE]);
        return 0;
    }
    DEBUG(LOG_INFO, "adding option 0x%02x", string[OPT_CODE]);
    memcpy(optionptr + end, string, string[OPT_LEN] + 2);
    optionptr[end + string[OPT_LEN] + 2] = DHCP_END;//补充END选项结尾
    return string[OPT_LEN] + 2;//返回所添加选项的整体长度
}

 将一个4字节的数据作为选项添加到options数组中 add_simple_option

/* add a one to four byte option to a packet */
/*
    将一个4字节的数据和code值组织为CLV格式存储起来
    得到的CLV结构数据交给add_option_string函数添加到options数组中
*/

int add_simple_option(unsigned char *optionptr, unsigned char code, u_int32_t data)
{
    char length = 0;
    int i;
    unsigned char option[2 + 4];
    unsigned char *u8;
    u_int16_t *u16;
    u_int32_t *u32;
    u_int32_t aligned;
    u8 = (unsigned char *) &aligned;
    u16 = (u_int16_t *) &aligned;
    u32 = &aligned;

    for (i = 0; options[i].code; i++)
        if (options[i].code == code) {
            length = option_lengths[options[i].flags & TYPE_MASK];
        }
        
    if (!length) {
        DEBUG(LOG_ERR, "Could not add option 0x%02x", code);
        return 0;
    }
    
    option[OPT_CODE] = code;
    option[OPT_LEN] = length;

    switch (length) {
        case 1: *u8 =  data; break;
        case 2: *u16 = data; break;
        case 4: *u32 = data; break;
    }
    memcpy(option + 2, &aligned, length);
    return add_option_string(optionptr, option);
}

注意:在options.c文件中还有两个函数分别是find_optionattach_option,这两个函数和上面的函数用处不一样,上面的这些函数是用于操作struct dhcpMessage报文结构中options[308]这个数组的,而这两个函数是在读取配置文件时操作struct server_config_t结构体中struct option_set *options成员的,这个成员将会保存配置文件中设置的opt选项(根据code值的升序链表)

  到这里,获取到dhcp报文和如何维护dhcp报文中的数据已经记录完了,下面就是根据获取到的报文决定dhcpd改如何动作,这部分'动作'是一定要按照dhcp协议规范来实现的。

 2.4 遵循协议规范的报文交互动作

  dhcp有几种报文类型,在客户端与服务器交互中这几种报文按照协议规定交互,可参考获得更多细节:

  参考网站:http://blog.csdn.net/u013485792/article/details/50731538

  下图是一个典型的客户端请求IP地址的报文交互,1234这4个报文是服务器的动作:

    结合参考网站可以很清晰的理解dhcp协议的运作流程。

  因为dhcpd是被动的,它等待客户端的连接,下图是服务器端收到报文之后动作的流程图:

 

  其实画完这个图我就后悔了,原因是我觉得把这个流程复杂化了,服务器收包后的处理过程看源代码应该更容易理解.

总结:

  服务器使用struct dhcpMessage结构体来接收收到的报文数据,get_packet函数是处理的开始,收到的报文根据报文的htype成员决定回复的动作是什么。options.c中的部分函数就是定义来方便访问struct dhcpMessage结构中options[308]成员。  

原文地址:https://www.cnblogs.com/Flychown/p/6826266.html