DHCP
基础知识
什么是DHCP
- DHCP:Dynamic Host Configuration Protocol 动态主机配置协议
- 作用:集中管理、分配IP地址,使得所管辖的网络环境中的主机动态的获取IP地址,Gateway地址、DNS地址等信息。
- 应用层协议(port: 67[Server]、68[Client])
为什么要使用DHCP
? (IP不够用)每台主机都需要分配唯一的IP地址,才可以进行网络通信。通过手动配置的情况往往会出现IP地址冲突导致通信异常。而DHCP也使得IP地址配置准确化,管理自动化,大大提升了效率。
- 准确的IP配置
- 减少IP地址冲突
- IP地址管理的自动化
- 高效的变更管理
IP地址分配机制
-
自动分配方式(Automatic Allocation) DHCP服务器为主机指定一个永久性的IP地址,一旦DHCP客户端第一次成功从DHCP服务器端租用到IP地址后,就可以永久性的使用该地址。 -
动态分配方式(Dynamic Allocation) DHCP服务器给主机指定一个具有时间限制的IP地址,时间到期或主机明确表示放弃该地址时,该地址可以被其他主机使用。 -
手工分配方式(Manual Allocation) 客户端的IP地址是由网络管理员指定的,DHCP服务器只是将指定的IP地址告诉客户端主机。
自动分配和动态分配的区别?
工作原理
报文类型
DHCP报文类型(Client–>Server) | 作用 |
---|
DHCP Discover | 客户端来查找可用的服务器 | DHCP Request | 客户端发送给服务器来请求配置参数或者请求配置确认或者续借租期 | DHCP Decline | 客户端发现地址已经被使用时,用来通知服务器 | DHCP Inform | 客户端已经有ip地址时来请求其他的配置参数 | DHCP Release | 客户端要释放地址时用来通知服务器 |
DHCP报文类型(Server–>Client) | 作用 |
---|
DHCP offer | 服务器用来响应客户端的DHCP Discover报文,并指定相应的配置参数 | DHCP ACK | 由服务器对客户端的Request报文的确认响应,含有配置参数acknowledge | DHCP NAK | 由服务器对客户端的Request报文的拒绝响应,告知请求失败,无法分配合适的IP地址 negative acknowledge |
工作大致步骤:
-
客户端通过广播发送DHCP Discover报文寻找服务器端 -
服务器通过单播发送DHCP Offer报文向客户提供IP地址等信息 -
客户端通过广播发送DHCP Request报文告知服务端本地选择使用哪个IP -
服务器通过单播发送DHCP Ack报文告知客户端IP地址是合法可用的 (无ip,服务器通过单播发送DHCP Nak报文告知客户端IP地址无法分配) -
客户端通过广播发送DHCP Release报文来释放IP地址
基本步骤
-
DHCP Discover
- source ip:0.0.0.0
- destination ip:255.255.255.255
- 含有Client的MAC地址(chaddr字段)
注意:请求参数列表中有 广播标志位flags,可以决定服务器单播0还是广播1发送响应报文
-
DHCP Offer
- source ip:DHCP server(option字段)
- destination ip:255.255.255.255(广播)/ Any(单播)
- 含有分配给客户端IP地址(yiaddr字段)等配置信息
-
DHCP Request
- source ip:0.0.0.0
- destination ip:255.255.255.255
- 含有选择了的DHCP Server和IP
-
DHCP ACK
中继
请求和提供阶段会有中继检查,之后两阶段基本一致
- DHCP Discover(Client)
- 检查报文hops字段(中继跳数),超过16,丢弃报文(每经过中继,hops字段+1)
- 检查报文giaddr字段,如果为0,设置为Discover报文的接口IP。
- giaddr填入第一个中继的IP,后面(中继)不再修改
- 目的IP改为下一中继的IP或者Server的IP
- 源IP改为中继连接Client的接口IP
- DHCP Offer(Server)
- 根据giaddr字段,分配同一网段的IP
- Server单播发送给giaddr指定的中继
- 中继检查是否一致,否则直接丢弃
重用IP
前提:Client并非首次连入该网络
在实际情况中,我们发现 DHCP Client 重启后,也能获得相同的 IP 地址。那是因为DHCP Server 为 DHCP Client 分配 IP 地址时,采用如下的顺序:
- DHCP Server 中与 DHCP Client 的 MAC 地址静态绑定的 IP 地址;
- DHCP Client 曾经使用过的 IP 地址;
- 最先找到的可用 IP 地址。
- 如果没找到可用的 IP 地址,就依次查询超过租期、发生冲突的 IP 地址,如果找到就进行分配,否则报错处理。
- DHCP Request(广播)
- DHCP ACK or DHCP NAK
- Server(根据MAC)查看记录,有记录则ACK,无记录则NAK或者沉默(和“我”无关)
租赁期限
-
T1(50%)
-
T2(87.5%)
-
100%
- Client停止使用当前IP,广播DHCP Discover
代码解析
文件作用
file name | role |
---|
arrping.c | 分配IP时候会检测IP是否冲突 | clientpacket.c | DHCP client报文的构造和发送 | clientsocket.c | DHCP client 套接字的建立 | common.c | 用于调试和记录日志以及其他帮助函数 | dhcpcmain.c | Client的入口 | dhcpdmain.c | Server的入口 | dhcprelay.c | DHCP中继 | domain_codec.c | | dumpleases.c | | files.c | 负责服务器的文件操作,通过read_config读取配置信息 | getopt32.c | 命令行参数解析(Busybox) | leases.c | 针对DHCP offer结构体的操作函数,服务器向客户端提供租约信息 | llist.c | 链表辅助函数 | options.c | 针对DHCP报文的options字段的操作函数 | packet.c | DHCP数据报文的构造和发送 | parse_config.c | 解析配置信息 | script.c | 调用DHCP client通知脚本的函数 | serverpacket.c | 数据报文发送之前,根据不同的情况对各个字段的填充相应的信息 | socket.c | 套接字的创建,interface信息的读取 | static_leases.c | 对在dhcpd.h里定义的struct static_lease结构体的相应操作函数 | udhcpc.c | DHCP client运行的主线,实现DHCP客户端的功能 | udhcpd.c | DHCP server运行的主线,实现DHCP服务器的功能 |
udhcpd.c
udhcp DHCP Server
能够接收并解析DHCP客户端直接发送或经由DHCP中继转发的 DHCP 请求报文,根据报文请求的内容并结合管理员的配置策略,为 DHCP 客户端进行一定范围内的IP 地址租约或网络参数分配,或对 IP 地址租约进行续约,或释放客户端不再需要的 IP 地址租约,从而保证该 IP 地址能够及时分配给其它有需要的 DHCP 客户端。
结构体
struct server_config_t {
uint32_t server; 我们的IP 网络字节序
#if ENABLE_FEATURE_UDHCP_PORT
uint16_t port;
#endif
uint32_t start_ip; 租赁的开始地址 主机字节序
uint32_t end_ip; 租赁的结束 主机字节序
struct option_set *options; 从配置文件加载的DHCP选项列表
char *interface; 要使用的接口名称
int ifindex; 要使用的接口索引号
uint8_t arp[6]; 我们的arp地址
char remaining; 租约剩余时间或者租约到期的时间
uint32_t lease; 租赁时间(秒)主机字节序
uint32_t max_leases; 最大组约数(包含保留地址)
uint32_t auto_time;
uint32_t decline_time; 如果客户端返回“”Deline“则保留地址多长时间
uint32_t conflict_time; ARP冲突应该租赁多长时间
uint32_t offer_time; 保留提供地址多长时间
uint32_t min_lease; 客户可要求的最低限度租约
char *lease_file;
char *pidfile;
char *notify_file; 每当编写租约时运行什么
char *offer_file; 每当发送ACK运行什么
uint32_t siaddr; 下一个服务器引导选项
char *sname; bootp 服务名称
char *boot_file; bootp引导文件选项
struct static_lease *static_leases; IP-MAC的列表来分配静态租赁
uint32_t static_netmask;
uint32_t static_router;
};
-
start_ip–end_ip 表示了可分配的地址空间 (IP地址池) -
struct option_set *options struct option_set {
uint8_t *data;
struct option_set *next;
};
选项集合(链表存储),决定了dhcpMessage中options的填写。 -
租赁相关 lease client请求的租赁期限最大值,>lease,就以lease为租赁期限; client请求未指明租赁期限,lease作为其租赁期限(静态租赁的默认租期)。 decline_time DHCP Decline发给server,server将IP地址添加到动态租赁数组中,租期为decline_time confict_time DHCP Offer之前serverIP检测,IP不可用,添加到动态租赁数组中,租期为confict_time offer_time DHCP Offer,server将IP-MAC对添加到动态租赁表里,还未确定,offer_time(默认60s) min_lease client请求租赁的最小时间 -
静态租赁 struct static_lease *static_lease struct static_lease{
uint8_t *mac;
uint32_t *ip;
struct static_lease *next;
};
DHCP的自动分配地址就是使用该结构体(也叫静态配置)
代码逻辑(流程)
-
set up parameters(server_config)
- read_config(files.c) 配置信息(租赁时间,IP地址池等等)
-
将pid写入pid_file
- write_pidfile(getopt32.c)
-
根据server_config的option设置lease
-
初始化和配置leases
-
读取接口信息
-
创建内部socket通信,注册信号处理器
- udcp_sp_setup(signalpipe.c) AF_UNIX
-
进入循环,进行DHCP的服务
-
建立socket监听和single处理器
这一步的主要目的就是对server_socket和socketpair建立监听,并根据对socketpair的信号情况及leases结构的内容,执行write_leases函数,该函数将最新的leases结构体里的内容写入lease_file,根据yiaddr、remaining和当前时间来更新lease_time,每次执行完write_leases函数和,都有更新time_end时刻,write_leases定义于files.c中。
max_sock = udhcp_sp_fd_set(&rfds, server_socket);
if (server_config.auto_time) {
tv.tv_sec = timeout_end - monotonic_sec();
tv.tv_usec = 0;
}
if (!server_config.auto_time || tv.tv_sec > 0) {
max_sock = server_socket > signal_pipe[0] ? server_socket : signal_pipe[0];
retval = select(max_sock + 1, &rfds, NULL, NULL,
server_config.auto_time ? &tv : NULL);
} else retval = 0;
if (retval == 0) {
write_leases();
timeout_end = monotonic_sec() + server_config.auto_time;
continue;
}
if (retval < 0 && errno != EINTR) {
DEBUG("error on select");
continue;
}
switch (udhcp_sp_read(&rfds)) {
case SIGUSR1:
bb_info_msg("Received a SIGUSR1");
write_leases();
timeout_end = monotonic_sec() + server_config.auto_time;
continue;
case SIGTERM:
bb_info_msg("Received a SIGTERM");
goto ret0;
case 0: break;
default: continue;
}
-
获取和提取报文
-
udhcp_get_kernel_packet -
get_option获取状态state信息 -
getIPbyMac寻找静态IP -
根据state和packet内容响应报文
udhcpc.c
udhcp DHCP client
用来获取 IP 地址或其它网络资源的主机,它会维持一个有限状态机,一旦启动 DHCP 客户端 DHCP 功能,它就会在状态机的控制下完成 IP 地址租约的申请、刷新和释放等功能,同时也会完成对其它自身所需的网络资源的获取。
结构体
struct dhcpMessage {
uint8_t op;
uint8_t htype;
uint8_t hlen;
uint8_t hops;
uint32_t xid;
uint16_t secs;
uint16_t flags;
uint32_t ciaddr;
uint32_t yiaddr;
uint32_t siaddr;
uint32_t giaddr;
uint8_t chaddr[16];
uint8_t sname[64];
uint8_t file[128];
uint32_t cookie;
uint8_t options[DHCP_OPTIONS_BUFSIZE + CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS];
} PACKED;
typedef struct client_config_tt {
uint8_t arp[6]; arp地址
合并标识字段为单个无符号opt
char no_default_options; 不包括默认的options请求
USE_FEATURE_UDHCP_PORT(uint16_t port;)
int ifindex; 使用接口的索引号
uint8_t opt_mask[256 / 8]; 发送选项的位掩码(-o选项)
const char *interface; 使用接口的名字
char *pidfile; 可选择地存储进程ID
const char *script; 在dhcp事件处运行的用户脚本
struct option_set *options; 要发送到服务器的DHCP选项列表
uint8_t *clientid; 要选择的客户端ID使用
uint8_t *vendorclass; 可选供应商类别-id使用
uint8_t *hostname; 可选主机名使用
uint8_t *fqdn; 可选的完全限定域名使用
} client_config_t;
代码逻辑(流程)
-
初始化和配置,对传入参数进行解析和处理 -
读取接口信息,udhcp_run_script -
建立socket和pipe,并进行监听 -
超时未收到信号
file.c
DHCP server file manipulation
结构体(read_config)
struct config_keyword {
const char *keyword;
int (*handler)(const char *line, void *var);
void *var;
const char *def;
};
关键:对照keyword,去var指定的位置进行对应的操作handler,填上line(也可以是def) 消息地图
static const struct config_keyword keywords[] = {
{"start", read_ip, &(server_config.start_ip), "192.168.0.20"},
{"end", read_ip, &(server_config.end_ip), "192.168.0.254"},
{"interface", read_str, &(server_config.interface), "eth0"},
{"max_leases", read_u32, &(server_config.max_leases), "235"},
{"remaining", read_yn, &(server_config.remaining), "yes"},
{"auto_time", read_u32, &(server_config.auto_time), "7200"},
{"decline_time", read_u32, &(server_config.decline_time), "3600"},
{"conflict_time",read_u32, &(server_config.conflict_time),"3600"},
{"offer_time", read_u32, &(server_config.offer_time), "60"},
{"min_lease", read_u32, &(server_config.min_lease), "60"},
{"lease_file", read_str, &(server_config.lease_file), LEASES_FILE},
{"pidfile", read_str, &(server_config.pidfile), "/var/run/udhcpd.pid"},
{"siaddr", read_ip, &(server_config.siaddr), "0.0.0.0"},
{"option", read_opt, &(server_config.options), ""},
{"opt", read_opt, &(server_config.options), ""},
{"notify_file", read_str, &(server_config.notify_file), ""},
{"offer_file", read_str, &(server_config.offer_file), ""},
{"sname", read_str, &(server_config.sname), ""},
{"boot_file", read_str, &(server_config.boot_file), ""},
{"static_lease", read_staticlease, &(server_config.static_leases), ""},
{"static_netmask", read_ip, &(server_config.static_netmask), ""},
{"static_router", read_ip, &(server_config.static_router), ""},
};
- 各信息操作函数的作用
- read_ip 将字符串格式的IP转换成IP格式
- read_u32 将字符串格式的数转换成数字
- read_yn line字符串yes:var(1),no:var(0)
- read_str 释放var原值,再根据line大小申请空间,赋新值
- read_opt line读取options,写到所指向的struct options_set链表中
- read_staticlease line读取MAC、IP地址,静态绑定写入static_lease链表中
- read_mac 将字符串格式的MAC转换成MAC
代码逻辑(流程)
- 准备读取
- 配置默认信息
- 从配置文件(/etc/udhcpd.conf)读取配置信息line
- 配置得到的信息(IP地址池、接口、租期等非默认信息)
函数关系
-
准备读取
-
配置默认信息
-
从配置文件(/etc/udhcpd.conf)读取配置信息 line(对应def,此处便是区别默认,该变量被封装在parser_t结构体中) tokens(对应keyword)
-
配置得到的信息(IP地址池、接口、租期等非默认信息)
- 使用3读取出来的配置信息,进行比对,然后使用操作函数处理
结构体(read_lease)
struct dhcpOfferedAddr {
uint8_t hostname[16]; 主机名
uint8_t chaddr[16]; Client MAC
uint32_t yiaddr; 租赁IP
uint32_t expires; 租赁IP的到期时间
uint8_t vendor[16];
};
目的:dhcp server启动后(可能是异常重启)读取文件,获取与租赁相关的信息
代码逻辑(流程)
函数关系
leases.c
tools to manage DHCP leases
结构体
struct dhcpOfferedAddr {
uint8_t hostname[16]; 主机名
uint8_t chaddr[16]; Client MAC
uint32_t yiaddr; 租赁IP
uint32_t expires; 租赁IP的到期时间
uint8_t vendor[16];
};
代码逻辑(模块)
-
add_lease -
find_lease_by_chaddr -
find_lease_by_yiaddr -
find_address
使用场景:该函数的调用是在,server端接收到DHCPDISOCVER的报文的时候,会为client提供一个IP地址:
-
server首先利用client的MAC地址在leases数组里查找该client以前是否在这里租赁过IP,租赁过的话,把以前的IP提供给client -
第一种情况不满足的话,server会检查DHCPDISCOVER报文的选项字段,client是否有请求的IP(该选项信息的CODE :DHCP_REQUESTED_IP),有的话检查该IP是否为Free,可以的话把Request IP提供给client。
上面两种情况都不满足的话,就调用find_address这个函数了
options.c
DHCP server option packet tools
结构体
struct dhcp_option {
uint8_t flags;
uint8_t code;
};
options字段存储三类数据:
- DHCP_PADDING 填充字节, 没有任何意义,填充 0x00
- DHCP_END potions字段结束的标志 0xFF
- 选项信息 对于DHCP过程真正有价值的信息,承载了选项数据(V)
代码逻辑
-
get_option * 参数struct dhcpMessage *packet DHCP报文
* int code需要获得什么选项信息(选项信息的标识)
*
* 返回指向选项信息的指针(去除了 OPT_CODE,OPT_LEN)
* 未找到返回NULL
*/
uint8_t *get_option(struct dhcpMessage *packet, int code)
{
int i, length;
uint8_t *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;
}
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;
}
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 + OPT_DATA];
i += optionptr[i + 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;
}
}
return NULL;
}
-
end_options
int end_option(uint8_t *optionptr)
{
int i = 0;
while (optionptr[i] != DHCP_END) {
if (optionptr[i] == DHCP_PADDING) i++;
else i += optionptr[i + OPT_LEN] + 2;
}
return i;
}
-
add_option_string /* add an option string to the options (an option string contains an option code,
* length, then data) */
int add_option_string(uint8_t *optionptr, uint8_t *string)
{
int end = end_option(optionptr);//找到DHCP_END在选项字段里偏移
/* 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 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;//在<CLV>的最后添加上DHCP_END
return string[OPT_LEN] + 2; //返回<CLV>长度
}
-
add_simple_option
int add_simple_option(uint8_t *optionptr, uint8_t code, uint32_t data)
{
struct dhcp_option *dh;
for (dh=dhcp_options; dh->code; dh++) {
if (dh->code == code) {
uint8_t option[6], len;
option[OPT_CODE] = code;
len = option_lengths[dh->flags & TYPE_MASK];
option[OPT_LEN] = len;
if (BB_BIG_ENDIAN) data <<= 8 * (4 - len);
memcpy(&option[OPT_DATA], &data, 4);
return add_option_string(optionptr, option);
}
}
DEBUG(LOG_ERR, "Could not add option 0x%02x", code);
return 0;
}
arpping.c
检测IP是否被使用
结构体
typedef struct arpMsg {
以太网帧头
uint8_t h_dest[6]; 目的MAC地址 FF-FF-FF-FF-FF-FF
uint8_t h_source[6]; 源MAC地址 发送ARP节点的MAC
uint16_t h_proto; 报文类型ID字段
uint16_t htype; 硬件类型 必须是ARPHRD_ETHER
uint16_t ptype; 协议类型 必须是ETH_P_IP(ip协议)
uint8_t hlen; MAC 6Byte
uint8_t plen; IP 4Byte
uint16_t operation; ARP 操作码 1 表示请求报文 2 表示应答报文
uint8_t sHaddr[6]; 发送方MAC
uint8_t sInaddr[4]; 发送方IP
uint8_t tHaddr[6]; 目标MAC
uint8_t tInaddr[4]; 目的IP
uint8_t pad[18]; 有效载荷(数据部分)
} PACKED;
代码逻辑(流程)
-
set socket -
send arp request -
wait for arp reply, and check it
函数关系
-
set socket
-
socket -
setsockopt_b(广播)
-
setsockopt
setsockopt函数用于任意类型、任意状态套接字接口的设置选项值。
-
send arp request
-
wait for arp reply, and check it
script.c
Functions to call the DHCP client notification scripts
代码逻辑(模块)
reference
- 一篇文章让你掌握神秘的 DHCP
- 计算机网络之DHCP原理与配置
- DHCP协议详解
感谢观看,侵权必删
|