servers/net: Make DHCP client more RFC 2131 compliant

* The client should enter state REBINDING only when RENEWING times
   out.
 * When in RENEWING or REBINDING state do not give up retrying
   unless the lease expires.
 * Fix bug sending 2^n DHCP requests at n-th lease renewal.
 * Use timeout values and renewal/rebinding times suggested by the
   RFC.
 * Use different XIDs in subsequent transactions.
This commit is contained in:
Pawel Dziepak
2013-07-25 04:05:10 +02:00
parent 3b2c8f50dc
commit 6972b91e17
3 changed files with 199 additions and 211 deletions
+187 -205
View File
@@ -16,6 +16,7 @@
#include <NetworkDevice.h> #include <NetworkDevice.h>
#include <NetworkInterface.h> #include <NetworkInterface.h>
#include <algorithm>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <errno.h> #include <errno.h>
#include <stdio.h> #include <stdio.h>
@@ -38,8 +39,10 @@
#define DHCP_CLIENT_PORT 68 #define DHCP_CLIENT_PORT 68
#define DHCP_SERVER_PORT 67 #define DHCP_SERVER_PORT 67
#define DEFAULT_TIMEOUT 2 // secs #define DEFAULT_TIMEOUT 4 // secs
#define MAX_TIMEOUT 15 // secs #define MAX_TIMEOUT 64 // secs
#define MAX_RETRIES 5
enum message_opcode { enum message_opcode {
BOOT_REQUEST = 1, BOOT_REQUEST = 1,
@@ -427,10 +430,7 @@ DHCPClient::DHCPClient(BMessenger target, const char* device)
fServer(AF_INET, NULL, DHCP_SERVER_PORT), fServer(AF_INET, NULL, DHCP_SERVER_PORT),
fLeaseTime(0) fLeaseTime(0)
{ {
fStartTime = system_time(); fTransactionID = (uint32)system_time() ^ rand();
fTransactionID = (uint32)fStartTime;
srand(fTransactionID);
BNetworkAddress link; BNetworkAddress link;
BNetworkInterface interface(device); BNetworkInterface interface(device);
@@ -498,13 +498,22 @@ DHCPClient::Initialize()
status_t status_t
DHCPClient::_Negotiate(dhcp_state state) DHCPClient::_Negotiate(dhcp_state state)
{ {
if (state == BOUND)
return B_OK;
fStartTime = system_time();
fTransactionID++;
char hostName[MAXHOSTNAMELEN];
if (gethostname(hostName, MAXHOSTNAMELEN) == 0)
fHostName.SetTo(hostName, MAXHOSTNAMELEN);
else
fHostName.Truncate(0);
int socket = ::socket(AF_INET, SOCK_DGRAM, 0); int socket = ::socket(AF_INET, SOCK_DGRAM, 0);
if (socket < 0) if (socket < 0)
return errno; return errno;
BNetworkAddress local;
local.SetToWildcard(AF_INET, DHCP_CLIENT_PORT);
// Enable reusing the port. This is needed in case there is more // Enable reusing the port. This is needed in case there is more
// than 1 interface that needs to be configured. Note that the only reason // than 1 interface that needs to be configured. Note that the only reason
// this works is because there is code below to bind to a specific // this works is because there is code below to bind to a specific
@@ -512,108 +521,69 @@ DHCPClient::_Negotiate(dhcp_state state)
int option = 1; int option = 1;
setsockopt(socket, SOL_SOCKET, SO_REUSEPORT, &option, sizeof(option)); setsockopt(socket, SOL_SOCKET, SO_REUSEPORT, &option, sizeof(option));
BNetworkAddress local;
local.SetToWildcard(AF_INET, DHCP_CLIENT_PORT);
option = 1;
setsockopt(socket, SOL_SOCKET, SO_BROADCAST, &option, sizeof(option));
if (bind(socket, local, local.Length()) < 0) { if (bind(socket, local, local.Length()) < 0) {
close(socket); close(socket);
return errno; return errno;
} }
BNetworkAddress broadcast;
broadcast.SetToBroadcast(AF_INET, DHCP_SERVER_PORT);
option = 1;
setsockopt(socket, SOL_SOCKET, SO_BROADCAST, &option, sizeof(option));
if (state == INIT) {
// The local interface does not have an address yet, bind the socket
// to the device directly.
BNetworkDevice device(Device());
int index = device.Index();
setsockopt(socket, SOL_SOCKET, SO_BINDTODEVICE, &index, sizeof(int));
}
bigtime_t previousLeaseTime = fLeaseTime; bigtime_t previousLeaseTime = fLeaseTime;
fLeaseTime = 0;
fRenewalTime = 0;
fRebindingTime = 0;
status_t status = B_ERROR;
time_t timeout;
uint32 tries;
_ResetTimeout(socket, timeout, tries);
char hostName[MAXHOSTNAMELEN];
if (gethostname(hostName, MAXHOSTNAMELEN) == 0)
fHostName.SetTo(hostName, MAXHOSTNAMELEN);
else
fHostName.Truncate(0);
dhcp_message discover(DHCP_DISCOVER);
_PrepareMessage(discover, state);
dhcp_message request(DHCP_REQUEST);
_PrepareMessage(request, state);
// send discover/request message
_SendMessage(socket, state == INIT ? discover : request,
state != RENEWING ? broadcast : fServer);
// no need to check the status; in case of an error we'll just send
// the message again
// receive loop until we've got an offer and acknowledged it
status_t status = B_OK;
while (state != BOUND) { while (state != BOUND) {
char buffer[2048]; status = _StateTransition(socket, state);
struct sockaddr_in from; if (status != B_OK && (state == SELECTING || state == REBOOTING))
socklen_t fromLength = sizeof(from); break;
ssize_t bytesReceived = recvfrom(socket, buffer, sizeof(buffer), }
0, (struct sockaddr*)&from, &fromLength);
if (bytesReceived < 0 && errno == B_TIMED_OUT) {
// depending on the state, we'll just try again
if (!_TimeoutShift(socket, timeout, tries)) {
close(socket); close(socket);
return B_TIMED_OUT;
if (fLeaseTime == 0)
fLeaseTime = previousLeaseTime;
if (fLeaseTime == 0)
fLeaseTime = 60;
if (fRenewalTime == 0)
fRenewalTime = fLeaseTime / 2;
if (fRebindingTime == 0)
fRebindingTime = fLeaseTime * 7 / 8;
fLeaseTime += fRequestTime;
fRenewalTime += fRequestTime;
fRebindingTime += fRequestTime;
_RestartLease(fRenewalTime);
fStatus = status;
if (status)
return status;
// configure interface
BMessage reply;
status = Target().SendMessage(&fConfiguration, &reply);
if (status == B_OK)
status = reply.FindInt32("status", &fStatus);
// configure resolver
reply.MakeEmpty();
fResolverConfiguration.AddString("device", Device());
status = Target().SendMessage(&fResolverConfiguration, &reply);
if (status == B_OK)
status = reply.FindInt32("status", &fStatus);
return status;
} }
_SendMessage(socket, state == INIT ? discover : request,
state != RENEWING ? broadcast : fServer);
continue;
} else if (bytesReceived < 0) status_t
break; DHCPClient::_GotMessage(dhcp_state& state, dhcp_message* message)
dhcp_message* message = (dhcp_message*)buffer;
if (message->transaction_id != htonl(fTransactionID)
|| !message->HasOptions()
|| memcmp(message->mac_address, discover.mac_address,
discover.hardware_address_length)) {
// this message is not for us
continue;
}
// advance from startup state
if (state == INIT)
state = SELECTING;
else if (state == INIT_REBOOT)
state = REBOOTING;
syslog(LOG_DEBUG, "%s: Received %s from %s\n",
Device(), dhcp_message::TypeToString(message->Type()),
_AddressToString(from.sin_addr.s_addr).String());
switch (message->Type()) {
case DHCP_NONE:
default:
// ignore this message
break;
case DHCP_OFFER:
{ {
// first offer wins switch (state) {
if (state != SELECTING) case SELECTING:
break; if (message->Type() == DHCP_OFFER) {
state = REQUESTING;
// collect interface options
fAssignedAddress = message->your_address; fAssignedAddress = message->your_address;
syslog(LOG_INFO, " your_address: %s\n", syslog(LOG_INFO, " your_address: %s\n",
@@ -630,27 +600,16 @@ DHCPClient::_Negotiate(dhcp_state state)
_ParseOptions(*message, address, fResolverConfiguration); _ParseOptions(*message, address, fResolverConfiguration);
fConfiguration.AddMessage("address", &address); fConfiguration.AddMessage("address", &address);
return B_OK;
// request configuration from the server
_ResetTimeout(socket, timeout, tries);
state = REQUESTING;
_PrepareMessage(request, state);
status = _SendMessage(socket, request, broadcast);
// we're sending a broadcast so that all potential offers
// get an answer
break;
} }
case DHCP_ACK: return B_BAD_VALUE;
{
if (state != REQUESTING
&& state != REBOOTING
&& state != REBINDING
&& state != RENEWING)
continue;
case REBOOTING:
case REBINDING:
case RENEWING:
case REQUESTING:
if (message->Type() == DHCP_ACK) {
// TODO: we might want to configure the stuff, don't we? // TODO: we might want to configure the stuff, don't we?
BMessage address; BMessage address;
fResolverConfiguration.MakeEmpty(); fResolverConfiguration.MakeEmpty();
@@ -661,67 +620,96 @@ DHCPClient::_Negotiate(dhcp_state state)
// our address request has been acknowledged // our address request has been acknowledged
state = BOUND; state = BOUND;
// configure interface return B_OK;
BMessage reply;
status = Target().SendMessage(&fConfiguration, &reply);
if (status == B_OK)
status = reply.FindInt32("status", &fStatus);
// configure resolver
reply.MakeEmpty();
fResolverConfiguration.AddString("device", Device());
status = Target().SendMessage(&fResolverConfiguration, &reply);
if (status == B_OK)
status = reply.FindInt32("status", &fStatus);
break;
} }
case DHCP_NACK: if (message->Type() == DHCP_NACK) {
if (state != REQUESTING
&& state != REBOOTING
&& state != REBINDING
&& state != RENEWING)
continue;
if (state == REBOOTING) {
// server reject our request on previous assigned address // server reject our request on previous assigned address
// back to square one... // back to square one...
fAssignedAddress = 0; fAssignedAddress = 0;
state = INIT;
return B_OK;
} }
// try again (maybe we should prefer other servers if this default:
// happens more than once) return B_BAD_VALUE;
status = _SendMessage(socket, discover, broadcast); }
if (status == B_OK) }
state = INIT;
status_t
DHCPClient::_StateTransition(int socket, dhcp_state& state)
{
if (state == INIT) {
// The local interface does not have an address yet, bind the socket
// to the device directly.
BNetworkDevice device(Device());
int index = device.Index();
setsockopt(socket, SOL_SOCKET, SO_BINDTODEVICE, &index, sizeof(int));
}
BNetworkAddress broadcast;
broadcast.SetToBroadcast(AF_INET, DHCP_SERVER_PORT);
time_t timeout;
uint32 tries;
_ResetTimeout(socket, state, timeout, tries);
dhcp_message discover(DHCP_DISCOVER);
_PrepareMessage(discover, state);
dhcp_message request(DHCP_REQUEST);
_PrepareMessage(request, state);
bool skipRequest = false;
dhcp_state originalState = state;
fRequestTime = system_time();
while (true) {
if (!skipRequest) {
_SendMessage(socket, originalState == INIT ? discover : request,
originalState == RENEWING ? fServer : broadcast);
if (originalState == INIT)
state = SELECTING;
else if (originalState == INIT_REBOOT)
state = REBOOTING;
}
char buffer[2048];
struct sockaddr_in from;
socklen_t fromLength = sizeof(from);
ssize_t bytesReceived = recvfrom(socket, buffer, sizeof(buffer),
0, (struct sockaddr*)&from, &fromLength);
if (bytesReceived < 0 && errno == B_TIMED_OUT) {
// depending on the state, we'll just try again
if (!_TimeoutShift(socket, state, timeout, tries))
return B_TIMED_OUT;
skipRequest = false;
continue;
} else if (bytesReceived < 0)
return errno;
skipRequest = true;
dhcp_message* message = (dhcp_message*)buffer;
if (message->transaction_id != htonl(fTransactionID)
|| !message->HasOptions()
|| memcmp(message->mac_address, discover.mac_address,
discover.hardware_address_length)) {
// this message is not for us
continue;
}
syslog(LOG_DEBUG, "%s: Received %s from %s\n",
Device(), dhcp_message::TypeToString(message->Type()),
_AddressToString(from.sin_addr.s_addr).String());
if (_GotMessage(state, message) == B_OK)
break; break;
} }
}
close(socket); return B_OK;
if (status == B_OK && fLeaseTime > 0) {
// notify early enough when the lease is
if (fRenewalTime == 0)
fRenewalTime = fLeaseTime * 2/3;
if (fRebindingTime == 0)
fRebindingTime = fLeaseTime * 5/6;
bigtime_t now = system_time();
_RestartLease(fRenewalTime);
fLeaseTime += now;
fRenewalTime += now;
fRebindingTime += now;
// make lease times absolute
} else {
fLeaseTime = previousLeaseTime;
bigtime_t now = system_time();
fRenewalTime = (fLeaseTime - now) * 2/3 + now;
fRebindingTime = (fLeaseTime - now) * 5/6 + now;
}
return status;
} }
@@ -732,7 +720,7 @@ DHCPClient::_RestartLease(bigtime_t leaseTime)
return; return;
BMessage lease(kMsgLeaseTime); BMessage lease(kMsgLeaseTime);
fRunner = new BMessageRunner(this, &lease, leaseTime, 1); fRunner = new BMessageRunner(this, &lease, leaseTime - system_time(), 1);
} }
@@ -899,7 +887,8 @@ DHCPClient::_PrepareMessage(dhcp_message& message, dhcp_state state)
void void
DHCPClient::_ResetTimeout(int socket, time_t& timeout, uint32& tries) DHCPClient::_ResetTimeout(int socket, dhcp_state& state, time_t& timeout,
uint32& tries)
{ {
timeout = DEFAULT_TIMEOUT; timeout = DEFAULT_TIMEOUT;
tries = 0; tries = 0;
@@ -912,15 +901,36 @@ DHCPClient::_ResetTimeout(int socket, time_t& timeout, uint32& tries)
bool bool
DHCPClient::_TimeoutShift(int socket, time_t& timeout, uint32& tries) DHCPClient::_TimeoutShift(int socket, dhcp_state& state, time_t& timeout,
uint32& tries)
{ {
timeout += timeout; if (state == RENEWING && system_time() > fRebindingTime) {
if (timeout > MAX_TIMEOUT) { state = REBINDING;
timeout = DEFAULT_TIMEOUT;
if (++tries > 2)
return false; return false;
} }
if (state == REBINDING && system_time() > fLeaseTime) {
state = INIT;
return false;
}
tries++;
timeout += timeout;
if (timeout > MAX_TIMEOUT)
timeout = MAX_TIMEOUT;
if (tries > MAX_RETRIES) {
bigtime_t remaining = 0;
if (state == RENEWING)
remaining = (fRebindingTime - system_time()) / 2 + 1;
else if (state == REBINDING)
remaining = (fLeaseTime - system_time()) / 2 + 1;
else
return false;
timeout = std::max(remaining / 1000000, bigtime_t(60));
}
syslog(LOG_DEBUG, "%s: Timeout shift: %lu secs (try %lu)\n", syslog(LOG_DEBUG, "%s: Timeout shift: %lu secs (try %lu)\n",
Device(), timeout, tries); Device(), timeout, tries);
@@ -978,13 +988,12 @@ DHCPClient::_CurrentState() const
{ {
bigtime_t now = system_time(); bigtime_t now = system_time();
if (now > fLeaseTime || fStatus < B_OK) if (now > fLeaseTime || fStatus != B_OK)
return INIT; return INIT;
if (now >= fRebindingTime) if (now >= fRebindingTime)
return REBINDING; return REBINDING;
if (now >= fRenewalTime) if (now >= fRenewalTime)
return RENEWING; return RENEWING;
return BOUND; return BOUND;
} }
@@ -994,35 +1003,8 @@ DHCPClient::MessageReceived(BMessage* message)
{ {
switch (message->what) { switch (message->what) {
case kMsgLeaseTime: case kMsgLeaseTime:
{ _Negotiate(_CurrentState());
dhcp_state state = _CurrentState();
bigtime_t next;
if (_Negotiate(state) == B_OK) {
switch (state) {
case RENEWING:
next = fRebindingTime;
break; break;
case REBINDING:
default:
next = fRenewalTime;
break;
}
} else {
switch (state) {
case RENEWING:
next = (fLeaseTime - fRebindingTime) / 4 + system_time();
break;
case REBINDING:
default:
next = (fLeaseTime - fRenewalTime) / 4 + system_time();
break;
}
}
_RestartLease(next - system_time());
break;
}
default: default:
BHandler::MessageReceived(message); BHandler::MessageReceived(message);
+8 -4
View File
@@ -44,6 +44,9 @@ public:
private: private:
status_t _Negotiate(dhcp_state state); status_t _Negotiate(dhcp_state state);
status_t _GotMessage(dhcp_state& state,
dhcp_message* message);
status_t _StateTransition(int socket, dhcp_state& state);
void _ParseOptions(dhcp_message& message, void _ParseOptions(dhcp_message& message,
BMessage& address, BMessage& address,
BMessage& resolverConfiguration); BMessage& resolverConfiguration);
@@ -52,10 +55,10 @@ private:
status_t _SendMessage(int socket, dhcp_message& message, status_t _SendMessage(int socket, dhcp_message& message,
const BNetworkAddress& address) const; const BNetworkAddress& address) const;
dhcp_state _CurrentState() const; dhcp_state _CurrentState() const;
void _ResetTimeout(int socket, time_t& timeout, void _ResetTimeout(int socket, dhcp_state& state,
uint32& tries); time_t& timeout, uint32& tries);
bool _TimeoutShift(int socket, time_t& timeout, bool _TimeoutShift(int socket, dhcp_state& state,
uint32& tries); time_t& timeout, uint32& tries);
void _RestartLease(bigtime_t lease); void _RestartLease(bigtime_t lease);
static BString _AddressToString(const uint8* data); static BString _AddressToString(const uint8* data);
@@ -71,6 +74,7 @@ private:
in_addr_t fAssignedAddress; in_addr_t fAssignedAddress;
BNetworkAddress fServer; BNetworkAddress fServer;
bigtime_t fStartTime; bigtime_t fStartTime;
bigtime_t fRequestTime;
bigtime_t fRenewalTime; bigtime_t fRenewalTime;
bigtime_t fRebindingTime; bigtime_t fRebindingTime;
bigtime_t fLeaseTime; bigtime_t fLeaseTime;
+2
View File
@@ -1388,6 +1388,8 @@ NetServer::_ConvertNetworkFromSettings(BMessage& message)
int int
main(int argc, char** argv) main(int argc, char** argv)
{ {
srand(system_time());
status_t status; status_t status;
NetServer server(status); NetServer server(status);
if (status != B_OK) { if (status != B_OK) {