前言:之前学习梳理dhcp有
(八十七) WiFi & DHCP
(九十) Android O 结合WifiStateMachine梳理WIFI DHCP流程
(一百零八)omnipeek 抓包尝试与简单分析 - CSDN博客
继续学习下DHCP的相关知识
参考:
DHCP的IP地址租约、释放
图解DHCP的4步租约过程
当DHCP客户端获取到一个IP地址后,并不代表可以永久使用这个地址,而是有一个使用期限,在DHCP中我们称之为租约期限,默认是自客户端成功获取之时算起,往后再推8天。其实除了这个8天的时间外,在有效的租约期限内,其实还包含着两个时间点,第四天和第七天,也就是租约的一半和租约的7/8。这三个时间点在DHCP 的Offer数据包中就有体现。如下图:
上图中标记红框处即为三个时间点。
我们再来打个比方。如果客户端在1月1日0时成功获取到一个IP地址,那么在DHCP管理器上就可以看到这条租约信息,对应的租约截止日期就是1月9日0时。当日期到默认租期的一半时,也就是第四天的时候。客户端会向DHCP服务器发送一个DHCP Request 的数据包,目的是请求更新自己的租约。如果DHCP服务器正常且响应了此请求,那么就会返回一个DHCP ACK的数据包,这表示续约成功。比如1月5日时,客户端提出续约申请,当DHCP服务器正常相应后,这台客户端的IP过期时间将延至1月13日,因为他是在5号提出的申请,判断是否续期和过期是以DHCP服务器上时间为准。将租期计算用画图的形式展现出来,如下图:
如果第一次没有续约成功,到了租期的7/8时,还会重复一次申请续约的过程。如果成功,新的租期自然是在申请日期的基础上加8天,以此类推。DHCP客户端获取到一个IP,只要是续约的时候顺利,那么它会一直使用这个IP地址,除非这个IP被排除或者被保留等。
如果在1/2租期申请更新,但没有得到DHCP的响应,怎么办? 比如这个IP被从作用域中移除,那么DHCP服务器会返回给客户端一个DHCP NACK的数据包。客户端收到这个数据包后会发送Discover的包查询,如果还是没得到回复,它就会继续使用原有的IP地址,当到7/8租期时间时就会再次申请租约更新。如果依然没有得到正确的回应,那只能得到租期截至后重新申请IP地址了。以上是有关租约的内容。
之前看代码dhcp完成后会进入到DhcpBoundState状态
class DhcpBoundState extends LoggingState {
@Override
public void enter() {
super.enter();
if (mDhcpLease.serverAddress != null && !connectUdpSock(mDhcpLease.serverAddress)) {
// There's likely no point in going into DhcpInitState here, we'll probably
// just repeat the transaction, get the same IP address as before, and fail.
//
// NOTE: It is observed that connectUdpSock() basically never fails, due to
// SO_BINDTODEVICE. Examining the local socket address shows it will happily
// return an IPv4 address from another interface, or even return "0.0.0.0".
//
// TODO: Consider deleting this check, following testing on several kernels.
notifyFailure();
transitionTo(mStoppedState);
}
scheduleLeaseTimers();
logTimeToBoundState();
}
@Override
public void exit() {
super.exit();
mLastBoundExitTime = SystemClock.elapsedRealtime();
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case CMD_RENEW_DHCP:
if (mRegisteredForPreDhcpNotification) {
transitionTo(mWaitBeforeRenewalState);
} else {
transitionTo(mDhcpRenewingState);
}
return HANDLED;
default:
return NOT_HANDLED;
}
}
private void logTimeToBoundState() {
long now = SystemClock.elapsedRealtime();
if (mLastBoundExitTime > mLastInitEnterTime) {
logState(DhcpClientEvent.RENEWING_BOUND, (int)(now - mLastBoundExitTime));
} else {
logState(DhcpClientEvent.INITIAL_BOUND, (int)(now - mLastInitEnterTime));
}
}
}
看下
private void scheduleLeaseTimers() {
if (mDhcpLeaseExpiry == 0) {
Log.d(TAG, "Infinite lease, no timer scheduling needed");
return;
}
final long now = SystemClock.elapsedRealtime();
// TODO: consider getting the renew and rebind timers from T1 and T2.
// See also:
// https://tools.ietf.org/html/rfc2131#section-4.4.5
// https://tools.ietf.org/html/rfc1533#section-9.9
// https://tools.ietf.org/html/rfc1533#section-9.10
final long remainingDelay = mDhcpLeaseExpiry - now;
final long renewDelay = remainingDelay / 2;
final long rebindDelay = remainingDelay * 7 / 8;
mRenewAlarm.schedule(now + renewDelay);
mRebindAlarm.schedule(now + rebindDelay);
mExpiryAlarm.schedule(now + remainingDelay);
Log.d(TAG, "Scheduling renewal in " + (renewDelay / 1000) + "s");
Log.d(TAG, "Scheduling rebind in " + (rebindDelay / 1000) + "s");
Log.d(TAG, "Scheduling expiry in " + (remainingDelay / 1000) + "s");
}
启动了3个倒计时,分别是renewal(1/2) rebind(7/8) expiry
class DhcpBoundState extends LoggingState {
@Override
public void enter() {
super.enter();
if (mDhcpLease.serverAddress != null && !connectUdpSock(mDhcpLease.serverAddress)) {
// There's likely no point in going into DhcpInitState here, we'll probably
// just repeat the transaction, get the same IP address as before, and fail.
//
// NOTE: It is observed that connectUdpSock() basically never fails, due to
// SO_BINDTODEVICE. Examining the local socket address shows it will happily
// return an IPv4 address from another interface, or even return "0.0.0.0".
//
// TODO: Consider deleting this check, following testing on several kernels.
notifyFailure();
transitionTo(mStoppedState);
}
scheduleLeaseTimers();
logTimeToBoundState();
}
@Override
public void exit() {
super.exit();
mLastBoundExitTime = SystemClock.elapsedRealtime();
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case CMD_RENEW_DHCP:
if (mRegisteredForPreDhcpNotification) {
transitionTo(mWaitBeforeRenewalState);
} else {
transitionTo(mDhcpRenewingState);
}
return HANDLED;
default:
return NOT_HANDLED;
}
}
收到CMD_RENEW_DHCP之后切换到DhcpRenewingState,暂时不管是否是waitbefore了
class DhcpRenewingState extends DhcpReacquiringState {
public DhcpRenewingState() {
mLeaseMsg = "Renewed";
}
@Override
public boolean processMessage(Message message) {
if (super.processMessage(message) == HANDLED) {
return HANDLED;
}
switch (message.what) {
case CMD_REBIND_DHCP:
transitionTo(mDhcpRebindingState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
protected Inet4Address packetDestination() {
// Not specifying a SERVER_IDENTIFIER option is a violation of RFC 2131, but...
// http://b/25343517 . Try to make things work anyway by using broadcast renews.
return (mDhcpLease.serverAddress != null) ?
mDhcpLease.serverAddress : INADDR_BROADCAST;
}
}
看下它的父类
abstract class DhcpReacquiringState extends PacketRetransmittingState {
protected String mLeaseMsg;
@Override
public void enter() {
super.enter();
startNewTransaction();
}
abstract protected Inet4Address packetDestination();
protected boolean sendPacket() {
return sendRequestPacket(
(Inet4Address) mDhcpLease.ipAddress.getAddress(), // ciaddr
INADDR_ANY, // DHCP_REQUESTED_IP
null, // DHCP_SERVER_IDENTIFIER
packetDestination()); // packet destination address
}
protected void receivePacket(DhcpPacket packet) {
if (!isValidPacket(packet)) return;
if ((packet instanceof DhcpAckPacket)) {
final DhcpResults results = packet.toDhcpResults();
if (results != null) {
if (!mDhcpLease.ipAddress.equals(results.ipAddress)) {
Log.d(TAG, "Renewed lease not for our current IP address!");
notifyFailure();
transitionTo(mDhcpInitState);
}
setDhcpLeaseExpiry(packet);
// Updating our notion of DhcpResults here only causes the
// DNS servers and routes to be updated in LinkProperties
// in IpManager and by any overridden relevant handlers of
// the registered IpManager.Callback. IP address changes
// are not supported here.
acceptDhcpResults(results, mLeaseMsg);
transitionTo(mDhcpBoundState);
}
} else if (packet instanceof DhcpNakPacket) {
Log.d(TAG, "Received NAK, returning to INIT");
notifyFailure();
transitionTo(mDhcpInitState);
}
}
}
DhcpRenewingState状态会发送dhcp request包,收到dhcpack包后会重新回到DhcpBoundState状态,并调用scheduleLeaseTimers重新对租期进行倒计时
如果收到dhcp nak,那就回到DhcpInitState,重新开始dhcp discover流程
DhcpRenewingState发送dhcp request没有回应拖到rebind time时间到的话,会切换到DhcpRebindingState
class DhcpRenewingState extends DhcpReacquiringState {
public DhcpRenewingState() {
mLeaseMsg = "Renewed";
}
@Override
public boolean processMessage(Message message) {
if (super.processMessage(message) == HANDLED) {
return HANDLED;
}
switch (message.what) {
case CMD_REBIND_DHCP:
transitionTo(mDhcpRebindingState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
protected Inet4Address packetDestination() {
// Not specifying a SERVER_IDENTIFIER option is a violation of RFC 2131, but...
// http://b/25343517 . Try to make things work anyway by using broadcast renews.
return (mDhcpLease.serverAddress != null) ?
mDhcpLease.serverAddress : INADDR_BROADCAST;
}
}
看下DhcpRebindingState
class DhcpRebindingState extends DhcpReacquiringState {
public DhcpRebindingState() {
mLeaseMsg = "Rebound";
}
@Override
public void enter() {
super.enter();
// We need to broadcast and possibly reconnect the socket to a
// completely different server.
closeQuietly(mUdpSock);
if (!initUdpSocket()) {
Log.e(TAG, "Failed to recreate UDP socket");
transitionTo(mDhcpInitState);
}
}
@Override
protected Inet4Address packetDestination() {
return INADDR_BROADCAST;
}
}
关闭并重启初始化udpsocket,然后发出dhcp request
class DhcpHaveLeaseState extends State {
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_EXPIRE_DHCP:
Log.d(TAG, "Lease expired!");
notifyFailure();
transitionTo(mDhcpInitState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
public void exit() {
// Clear any extant alarms.
mRenewAlarm.cancel();
mRebindAlarm.cancel();
mExpiryAlarm.cancel();
clearDhcpState();
// Tell IpManager to clear the IPv4 address. There is no need to
// wait for confirmation since any subsequent packets are sent from
// INADDR_ANY anyway (DISCOVER, REQUEST).
mController.sendMessage(CMD_CLEAR_LINKADDRESS);
}
}
expire time到了后会报告租约过期了,并切换到DhcpInitState重新开始dhcp discover流程。