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_option和attach_option,这两个函数和上面的函数用处不一样,上面的这些函数是用于操作struct dhcpMessage报文结构中options[308]这个数组的,而这两个函数是在读取配置文件时操作struct server_config_t结构体中struct option_set *options成员的,这个成员将会保存配置文件中设置的opt选项(根据code值的升序链表)
到这里,获取到dhcp报文和如何维护dhcp报文中的数据已经记录完了,下面就是根据获取到的报文决定dhcpd改如何动作,这部分'动作'是一定要按照dhcp协议规范来实现的。
2.4 遵循协议规范的报文交互动作
dhcp有几种报文类型,在客户端与服务器交互中这几种报文按照协议规定交互,可参考获得更多细节:
参考网站:http://blog.****.net/u013485792/article/details/50731538
下图是一个典型的客户端请求IP地址的报文交互,1234这4个报文是服务器的动作:
结合参考网站可以很清晰的理解dhcp协议的运作流程。
因为dhcpd是被动的,它等待客户端的连接,下图是服务器端收到报文之后动作的流程图:
其实画完这个图我就后悔了,原因是我觉得把这个流程复杂化了,服务器收包后的处理过程看源代码应该更容易理解.
总结:
服务器使用struct dhcpMessage结构体来接收收到的报文数据,get_packet函数是处理的开始,收到的报文根据报文的htype成员决定回复的动作是什么。options.c中的部分函数就是定义来方便访问struct dhcpMessage结构中options[308]成员。