目錄
- 一、DHCP(Dynamic Host Configuration Protocol)
- 1.1 前置知識
- 1.2 參考鏈接
- 1.3 IP地址分配代碼分析
- rfc2131.c
- dhcp-common.c
- dhcp.c
- 1.4 幾個小問題
- 1.4.1 連續IP模式(sequential_ip)
- 1.4.2 重新連接使用IP地址
- 1.4.3 續約租期
- 1.4.4 不同的MAC地址分配到相同IP
一、DHCP(Dynamic Host Configuration Protocol)
1.1 前置知識
之前也學習了一下,總結了一些概念和抓包分析,此處不贅述。
DHCP和PPPoE協議以及抓包分析
1.2 參考鏈接
24-Openwrt dnsmasq
DNS and DHCP configuration
rfc2131文檔
DHCP協議詳解
1.3 IP地址分配代碼分析
本次著重看了這一塊代碼,其它部分再后續補充。
吐槽一下:源代碼的格式真是一言難盡,縮進亂七八糟,而且有的空格有的tab看著也難受,格式不標準,有的都不能正確縮放,還是讓GPT轉化了一下再看的。
rfc2131.c
if (mess_type == 0 && !pxe) {/* BOOTP request */struct dhcp_netid id, bootp_id;struct in_addr *logaddr = NULL;/* must have a MAC addr for bootp */if (mess->htype == 0 || mess->hlen == 0 || (context->flags & CONTEXT_PROXY))return 0;if (have_config(config, CONFIG_DISABLE))message = _("disabled");end = mess->options + 64; /* BOOTP vend area is only 64 bytes *///如果配置中設置了 CONFIG_NAME 標志,則設置主機名(hostname)和域名(domain)。if (have_config(config, CONFIG_NAME)) {hostname = config->hostname;domain = config->domain;}
//遍歷配置中的網絡標識列表(netid),并將它們添加到當前的 netid 列表中if (config) {struct dhcp_netid_list *list;for (list = config->netid; list; list = list->next) {list->list->next = netid;netid = list->list;}}/* Match incoming filename field as a netid. */if (mess->file[0]) {memcpy(daemon->dhcp_buff2, mess->file, sizeof(mess->file));daemon->dhcp_buff2[sizeof(mess->file) + 1] = 0; /* ensure zero term. */id.net = (char *)daemon->dhcp_buff2;id.next = netid;netid = &id;}/* Add "bootp" as a tag to allow different options, address ranges etc for BOOTP clients */bootp_id.net = "bootp";bootp_id.next = netid;netid = &bootp_id;//運行標識的處理函數(run_tag_if)以獲取最終的 netid 列表。tagif_netid = run_tag_if(netid);//遍歷 dhcp_ignore 列表,如果與 netid 列表中的標識匹配,則將消息設置為 "ignored"。for (id_list = daemon->dhcp_ignore; id_list; id_list = id_list->next) {if (match_netid(id_list->list, tagif_netid, 0)) {message = _("ignored");break;}}if (!message) {int nailed = 0;
//檢查是否已配置 IP 地址(have_config(config, CONFIG_ADDR))。如果配置了,嘗試分配指定的 IP 地址。if (have_config(config, CONFIG_ADDR)) {nailed = 1;logaddr = &config->addr;mess->yiaddr = config->addr;if ((lease = lease_find_by_addr(config->addr)) &&(lease->hwaddr_len != mess->hlen ||lease->hwaddr_type != mess->htype ||memcmp(lease->hwaddr, mess->chaddr, lease->hwaddr_len) != 0)){message = _("address in use");}}else {
//如果沒有配置 IP 地址,嘗試從已分配的地址中查找與客戶端 MAC 地址匹配的租約(lease_find_by_client)if (!(lease = lease_find_by_client(mess->chaddr, mess->hlen, mess->htype, NULL, 0)) ||!address_available(context, lease->addr, tagif_netid)){if (lease) {/* lease exists, wrong network. */lease_prune(lease, now);lease = NULL;}
//如果沒有找到匹配的租約或該地址不可用(address_available),則嘗試分配一個新的 IP 地址if (!address_allocate(context, &mess->yiaddr, mess->chaddr, mess->hlen, tagif_netid, now, loopback)) {message = _("no address available");}}else {mess->yiaddr = lease->addr;}}//如果分配了 IP 地址,檢查該地址是否在正確的網絡上(narrow_context)。如果不在正確的網絡上,設置錯誤消息為 "wrong network"。if (!message && !(context = narrow_context(context, mess->yiaddr, netid))) {message = _("wrong network");}//更新 netid 列表并重新運行標識處理函數(run_tag_if)。else if (context->netid.net) {context->netid.next = netid;tagif_netid = run_tag_if(&context->netid);}log_tags(tagif_netid, ntohl(mess->xid));//遍歷 bootp_dynamic 列表,檢查是否配置了與 netid 列表匹配的地址。如果沒有找到,則設置錯誤消息為 "no address configured"。if (!message && !nailed) {for (id_list = daemon->bootp_dynamic; id_list; id_list = id_list->next) {if ((!id_list->list) || match_netid(id_list->list, tagif_netid, 0)) {break;}}if (!id_list) {message = _("no address configured");}}//如果沒有錯誤消息,并且沒有租約可用(!lease),嘗試為分配的 IP 地址創建一個租約(lease4_allocate)。if (!message && !lease && !(lease = lease4_allocate(mess->yiaddr))) {message = _("no leases left");}//如果成功分配租約,設置租約的硬件地址、主機名、租約到期時間等信息。if (!message) {logaddr = &mess->yiaddr;lease_set_hwaddr(lease, mess->chaddr, NULL, mess->hlen, mess->htype, 0, now, 1);if (hostname) {lease_set_hostname(lease, hostname, 1, get_domain(lease->addr), domain);}/* infinite lease unless nailed in dhcp-host line. */lease_set_expires(lease, have_config(config, CONFIG_TIME) ? config->lease_time : 0xffffffff, now);lease_set_interface(lease, int_index, now);//清空消息中的選項(do_options),為客戶端提供配置選項。clear_packet(mess, end);do_options(context, mess, end, NULL, hostname, get_domain(mess->yiaddr), netid, subnet_addr, 0, 0, -1, NULL, vendor_class_len, now, 0xffffffff, 0);}}daemon->metrics[METRIC_BOOTP]++;log_packet("BOOTP", logaddr, mess->chaddr, mess->hlen, iface_name, NULL, message, mess->xid);return message ? 0 : dhcp_packet_size(mess, agent_id, real_end);
}
然后看一個各個函數的具體實現:
dhcp-common.c
//根據匹配條件更新給定的網絡標識列表,并重新運行標識處理邏輯。
struct dhcp_netid *run_tag_if(struct dhcp_netid *tags)
{struct tag_if *exprs;struct dhcp_netid_list *list;// 遍歷服務器中定義的標識處理表達式(tag_if)for (exprs = daemon->tag_if; exprs; exprs = exprs->next) {// 檢查當前標識處理表達式是否與當前網絡標識列表匹配if (match_netid(exprs->tag, tags, 1)) {// 對于匹配的表達式,遍歷操作集合(set)for (list = exprs->set; list; list = list->next) {// 將操作列表中的操作添加到網絡標識列表(tags)的開頭list->list->next = tags;tags = list->list;}}}// 返回更新后的網絡標識列表(tags)return tags;
}
dhcp.c
struct dhcp_context *address_available(struct dhcp_context *context,struct in_addr taddr,struct dhcp_netid *netids)
{/* 檢查地址是否適用于此網絡,檢查所有可能的范圍。確保該地址沒有被服務器自身使用。 */unsigned int start, end, addr = ntohl(taddr.s_addr);struct dhcp_context *tmp;// 檢查提供的地址是否與服務器的路由器地址匹配for (tmp = context; tmp; tmp = tmp->current) {if (taddr.s_addr == context->router.s_addr) {return NULL; // 服務器的路由器地址,不可用}}// 遍歷每個上下文及其地址范圍,以查找可用的地址for (tmp = context; tmp; tmp = tmp->current) {start = ntohl(tmp->start.s_addr);end = ntohl(tmp->end.s_addr);// 檢查地址是否在當前上下文的范圍內且匹配給定的網絡標識if (!(tmp->flags & (CONTEXT_STATIC | CONTEXT_PROXY)) &&addr >= start &&addr <= end &&match_netid(tmp->filter, netids, 1)) {return tmp; // 地址在此上下文中可用}}return NULL; // 地址不可用
}
int address_allocate(struct dhcp_context *context,struct in_addr *addrp, unsigned char *hwaddr, int hw_len,struct dhcp_netid *netids, time_t now, int loopback)
{/* 尋找一個可用的地址:排除正在使用的地址和分配給特定硬件地址/客戶端標識/主機名的地址首先嘗試返回與netids匹配的上下文。 */struct in_addr start, addr;struct dhcp_context *c, *d;int i, pass;unsigned int j;/* 對hwaddr進行哈希:使用SDBM哈希算法。即使在具有類似值的“字符串”上,似乎也能得到良好的分散效果。 */for (j = 0, i = 0; i < hw_len; i++)j = hwaddr[i] + (j << 6) + (j << 16) - j;/* j == 0 是標記 */if (j == 0)j = 1;for (pass = 0; pass <= 1; pass++)for (c = context; c; c = c->current) {if (c->flags & (CONTEXT_STATIC | CONTEXT_PROXY))continue;else if (!match_netid(c->filter, netids, pass))continue;else {if (option_bool(OPT_CONSEC_ADDR))/* 種子是此上下文中最大的現存租約地址 */start = lease_find_max_addr(c);else/* 基于hwaddr選擇種子 */start.s_addr = htonl(ntohl(c->start.s_addr) +((j + c->addr_epoch) % (1 + ntohl(c->end.s_addr) - ntohl(c->start.s_addr))));/* 循環,直到找到一個可用的地址。 */addr = start;do {/* 排除服務器正在使用的地址。 */for (d = context; d; d = d->current) {if (addr.s_addr == d->router.s_addr) {break;}}/* 以.255和.0結尾的地址在Windows上有問題,即使使用超網也是如此。例如,dhcp-range=192.168.0.1,192.168.1.254,255,255,254.0那么192.168.0.255是有效的IP地址,但在Windows上卻不是,因為它在C類范圍內。請參閱KB281579。因此,我們不分配這些地址,以避免難以診斷的問題。感謝Bill。 */if (!d &&!lease_find_by_addr(addr) &&!config_find_by_address(daemon->dhcp_conf, addr) &&(!IN_CLASSC(ntohl(addr.s_addr)) ||((ntohl(addr.s_addr) & 0xff) != 0xff && ((ntohl(addr.s_addr) & 0xff) != 0x0)))) {/* 在連續IP模式下,跳過等于客戶端拒絕的地址數量的地址。這應該避免同一客戶端在拒絕地址后再次被分配相同的地址。 */if (option_bool(OPT_CONSEC_ADDR) && c->addr_epoch) {c->addr_epoch--;} else {struct ping_result *r;if ((r = do_icmp_ping(now, addr, j, loopback))) {/* 連續IP模式:我們最近為另一個客戶端提供了此地址(不同的哈希),不要再次提供給此客戶端。 */if (!option_bool(OPT_CONSEC_ADDR) || r->hash == j) {*addrp = addr;return 1;}} else {/* 地址正在使用中:擾動地址選擇,以便不太可能再次嘗試此地址。 */if (!option_bool(OPT_CONSEC_ADDR)) {c->addr_epoch++;}}}}addr.s_addr = htonl(ntohl(addr.s_addr) + 1);if (addr.s_addr == htonl(ntohl(c->end.s_addr) + 1)) {addr = c->start;}} while (addr.s_addr != start.s_addr);}}return 0; // 無可用地址
}
上述算法過程舉例如下,假設有一個 DHCP 上下文 c
,其中包含以下信息:
c->start.s_addr
表示分配地址的起始地址,假設為 192.168.1.100(以網絡字節序表示)。c->end.s_addr
表示分配地址的結束地址,假設為 192.168.1.200(以網絡字節序表示)。c->addr_epoch
是一個地址時代值,假設為 5。j
是之前計算得到的 SDBM 哈希值,假設為 12345。
現在我們來演示如何通過上述代碼生成一個新的分配地址:
-
計算地址范圍內的總地址數:
1 + ntohl(c->end.s_addr) - ntohl(c->start.s_addr) = 1 + 192.168.1.200 - 192.168.1.100 = 101
。 -
計算 SDBM 哈希值和地址時代的影響:
(j + c->addr_epoch) % (1 + 101) = (12345 + 5) % 102 = 12350 % 102 = 46
。 -
計算新的分配地址的偏移量:
ntohl(c->start.s_addr) + 46 = 3232235876 + 46 = 3232235922
。 -
將計算得到的偏移量轉換為網絡字節序:
htonl(3232235922) = 192.168.1.122
。
因此,根據上述計算,生成的新分配地址將是 192.168.1.122。這個過程確保了生成的地址在指定的地址范圍內,同時通過 SDBM 哈希值和地址時代進行了調整。
struct dhcp_context *narrow_context(struct dhcp_context *context,struct in_addr taddr,struct dhcp_netid *netids)
{/* 我們從一組可能的上下文開始,所有這些上下文都在當前的物理接口上。這些上下文通過 ->current 進行鏈接。在這里,我們有一個地址,并返回與該地址對應的實際上下文。請注意,如果地址來自dhcp-host,并且位于任何dhcp-range之外,則可能沒有匹配的上下文。在這種情況下,如果可能的話,我們會返回靜態范圍,或者如果失敗的話,返回正確子網上的任何上下文。(如果有多個上下文,這是一個不穩定的配置:也許應該有一個警告。) */struct dhcp_context *tmp;if (!(tmp = address_available(context, taddr, netids))) {for (tmp = context; tmp; tmp = tmp->current) {if (match_netid(tmp->filter, netids, 1) &&is_same_net(taddr, tmp->start, tmp->netmask) &&(tmp->flags & CONTEXT_STATIC)) {break;}}if (!tmp) {for (tmp = context; tmp; tmp = tmp->current) {if (match_netid(tmp->filter, netids, 1) &&is_same_net(taddr, tmp->start, tmp->netmask) &&!(tmp->flags & CONTEXT_PROXY)) {break;}}}}/* 現在只允許一個上下文 */if (tmp) {tmp->current = NULL;}return tmp;
}
narrow_context
的函數,用于根據給定的地址和網絡標識來縮小上下文的范圍。它首先嘗試根據給定的地址和網絡標識來查找可用的上下文。如果找不到匹配的可用上下文,則會嘗試在同一子網中查找靜態范圍,或者如果沒有靜態范圍,則查找任何與正確子網匹配的上下文。最后,函數會將選定的上下文鏈表中的 current 指針設置為 NULL,以確保只返回一個上下文。函數將返回選定的上下文,如果找不到合適的上下文,則返回 NULL。
1.4 幾個小問題
1.4.1 連續IP模式(sequential_ip)
上述代碼中有提到這個模式,openwrt中對應的配置字段就是sequential_ip。對應的描述如下:
Name | Type | Default | Option | Description |
---|---|---|---|---|
sequential_ip | boolean | 0 | –dhcp-sequential-ip | Dnsmasq is designed to choose IP addresses for DHCP clients using a hash of the client’s MAC address. This normally allows a client’s address to remain stable long-term, even if the client sometimes allows its DHCP lease to expire. In this default mode IP addresses are distributed pseudo-randomly over the entire available address range. There are sometimes circumstances (typically server deployment) where it is more convenient to have IP addresses allocated sequentially, starting from the lowest available address, and setting this parameter enables this mode. Note that in the sequential mode, clients which allow a lease to expire are much more likely to move IP address; for this reason it should not be generally used. |
Dnsmasq用于使用客戶端MAC地址的哈希為DHCP客戶端選擇IP地址。這通常允許客戶端的地址長期保持穩定,即使客戶端有時允許其DHCP租約到期。在此默認模式下,IP地址在整個可用地址范圍內偽隨機分布。有時在某些情況下(通常是服務器部署),從最低可用地址開始按順序分配IP地址更方便,并且設置此參數可以啟用此模式。請注意,在順序模式中,允許租約到期的客戶端更有可能移動IP地址;由于這個原因,它不應該被普遍使用。 |
可以看到按順序分配,設置成功
1.4.2 重新連接使用IP地址
在RFC 2131文檔第3.2節描述了客戶端重新使用先前分配的網絡地址時的客戶端-服務器交互過程:
根據文檔中的描述,如果客戶端希望重新連接并使用先前分配的網絡地址,客戶端可以選擇省略先前部分描述的一些步驟。客戶端可以通過向服務器發送DHCPREQUEST
消息來請求使用先前分配的網絡地址。在DHCPREQUEST消息中,客戶端將其網絡地址填入“請求的IP地址”
選項中,并且不填寫“ciaddr”
字段。服務器收到DHCPREQUEST消息后,如果具有客戶端的配置參數信息,則會向客戶端發送DHCPACK消息
。服務器不應檢查客戶端的網絡地址是否已被使用
。客戶端在收到DHCPACK消息后,將配置參數應用于自身,并記錄DHCPACK消息中指定的租約持續時間。此時,客戶端已經重新連接并配置完成。
請注意,根據文檔中的描述,如果客戶端在DHCPACK消息中檢測到分配的IP地址已經被使用
,則客戶端必須發送DHCPDECLINE消息
給服務器,并重新啟動配置過程以請求新的網絡地址。如果客戶端收到DHCPNAK消息
,則表示無法重新使用先前分配的網絡地址,客戶端必須重新啟動配置過程
,并按照文檔中描述的完整流程進行操作。
重用以前分配的網絡地址時,DHCP客戶端和服務器之間交換的消息時間軸圖
注:客戶端通常不會在正常關閉期間放棄其租約。只有在客戶端明確需要放棄其租期的情況下,例如,客戶端即將移動到另一個子網,客戶端才會發送DHCPRELEASE消息。
1.4.3 續約租期
在RFC 2131文檔的第4.4.5節中描述了租期續約的過程:
-
客戶端在T1之前選擇續約或延長租期:客戶端可以選擇在T1之前續約或延長租期。這意味著客戶端可以在租期即將到期之前主動向服務器發送DHCPREQUEST消息來請求續約。
-
服務器根據網絡管理員設置的策略來決定是否延長租期:服務器可以根據網絡管理員設置的策略來決定是否延長客戶端的租期。這意味著服務器可以根據自己的策略來決定是否接受客戶端的續約請求。
-
服務器應返回調整后的T1和T2的值:服務器應返回調整后的T1和T2的值,以考慮租期剩余的時間。這意味著服務器應根據租期剩余時間來調整T1和T2的值,并將其返回給客戶端。
-
在續約和重新綁定狀態下,如果客戶端收不到DHCPREQUEST消息的響應,客戶端應等待剩余時間的一半(在續約狀態下是T2的一半,在重新綁定狀態下是租期剩余時間的一半),直到最少等待60秒,然后重新發送DHCPREQUEST消息。
具體有以下三種情況:
(1)當clientIP地址已經用到50%的時間,續租一下,client端就會以單播形式向服務端發送一個DHCP Request包,當server響應時就會回應一個ACK包,會重新約定一個時間。
(2)當clientIP地址已經用到50%的時間,續租一下,client端就會以單播形式向服務端發送一個DHCP Request包,server沒有響應,client會繼續使用,當使用到87.5%時,會在續租一次,同時就以廣播的方式是發送一個request包,server這時收到響應以后,就會回應一個ACK包,重新約定一個時間。
(3)當clientIP地址已經用到50%的時間,續租一下,client端就會以單播形式向服務端發送一個DHCP Request包,server沒有響應,client會繼續使用,當使用到87.5%時,會在續租一次,同時就以廣播的方式是發送一個request包,如果server還是沒有響應,client那就直接使用到過期。
DHCP客戶端的狀態轉換圖
1.4.4 不同的MAC地址分配到相同IP
這是無意間收到的一個提問,可以參考學習。
multiple offers with same IP to different MAC addresses