• 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]成员。  

  • 相关阅读:
    Solr4.8.0源码分析(12)之Lucene的索引文件(5)
    JAVA基础(1)之hashCode()
    Solr4.8.0源码分析(11)之Lucene的索引文件(4)
    检查数据不一致脚本
    索引的新理解
    数据库放到容器里
    动态规划
    每天晚上一个动态规划
    postgresql parallel join example
    名不正,则言不顺
  • 原文地址:https://www.cnblogs.com/Flychown/p/6826266.html
Copyright © 2020-2023  润新知