mirror of
https://github.com/openvswitch/ovs
synced 2025-10-23 14:57:06 +00:00
This builds on earlier work that implemented netdev object refcounting. However, rather than requiring explicit create and destroy calls, these operations are now performed automatically based on the referenece count. This is important because in certain situations it is not possible to know whether a netdev has already been created. A workaround existed (which looked fairly similar to this paradigm) but introduced it's own issues. This simplifies and unifies the API.
1084 lines
32 KiB
C
1084 lines
32 KiB
C
/*
|
||
* Copyright (c) 2008, 2009, 2010 Nicira Networks.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at:
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
#include <config.h>
|
||
#include "dhcp-client.h"
|
||
#include <arpa/inet.h>
|
||
#include <assert.h>
|
||
#include <errno.h>
|
||
#include <inttypes.h>
|
||
#include <limits.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/types.h>
|
||
#include <time.h>
|
||
#include <unistd.h>
|
||
#include "csum.h"
|
||
#include "dhcp.h"
|
||
#include "dynamic-string.h"
|
||
#include "flow.h"
|
||
#include "netdev.h"
|
||
#include "ofpbuf.h"
|
||
#include "poll-loop.h"
|
||
#include "sat-math.h"
|
||
#include "timeval.h"
|
||
|
||
#define THIS_MODULE VLM_dhcp_client
|
||
#include "vlog.h"
|
||
|
||
#define DHCLIENT_STATES \
|
||
DHCLIENT_STATE(INIT, 1 << 0) \
|
||
DHCLIENT_STATE(INIT_REBOOT, 1 << 1) \
|
||
DHCLIENT_STATE(REBOOTING, 1 << 2) \
|
||
DHCLIENT_STATE(SELECTING, 1 << 3) \
|
||
DHCLIENT_STATE(REQUESTING, 1 << 4) \
|
||
DHCLIENT_STATE(BOUND, 1 << 5) \
|
||
DHCLIENT_STATE(RENEWING, 1 << 6) \
|
||
DHCLIENT_STATE(REBINDING, 1 << 7) \
|
||
DHCLIENT_STATE(RELEASED, 1 << 8)
|
||
enum dhclient_state {
|
||
#define DHCLIENT_STATE(NAME, VALUE) S_##NAME = VALUE,
|
||
DHCLIENT_STATES
|
||
#undef DHCLIENT_STATE
|
||
};
|
||
|
||
static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(60, 60);
|
||
|
||
static const char *
|
||
state_name(enum dhclient_state state)
|
||
{
|
||
switch (state) {
|
||
#define DHCLIENT_STATE(NAME, VALUE) case S_##NAME: return #NAME;
|
||
DHCLIENT_STATES
|
||
#undef DHCLIENT_STATE
|
||
}
|
||
return "***ERROR***";
|
||
}
|
||
|
||
struct dhclient {
|
||
/* Configuration. */
|
||
struct netdev *netdev;
|
||
|
||
void (*modify_request)(struct dhcp_msg *, void *aux);
|
||
bool (*validate_offer)(const struct dhcp_msg *, void *aux);
|
||
void *aux;
|
||
|
||
/* DHCP state. */
|
||
enum dhclient_state state;
|
||
unsigned int state_entered; /* When we transitioned to this state. */
|
||
uint32_t xid; /* In host byte order. */
|
||
uint32_t ipaddr, netmask, router;
|
||
uint32_t server_ip;
|
||
struct dhcp_msg *binding;
|
||
bool changed;
|
||
|
||
unsigned int retransmit, delay; /* Used by send_reliably(). */
|
||
unsigned int max_timeout;
|
||
|
||
unsigned int init_delay; /* Used by S_INIT. */
|
||
|
||
time_t lease_expiration;
|
||
unsigned int bound_timeout;
|
||
unsigned int renewing_timeout;
|
||
unsigned int rebinding_timeout;
|
||
|
||
/* Used by dhclient_run() and dhclient_wait() */
|
||
unsigned int min_timeout;
|
||
int received;
|
||
|
||
/* Set when we send out a DHCPDISCOVER message. */
|
||
uint32_t secs;
|
||
|
||
struct ds s;
|
||
};
|
||
|
||
/* Minimum acceptable lease time, in seconds. */
|
||
#define MIN_ACCEPTABLE_LEASE 15
|
||
|
||
static void state_transition(struct dhclient *, enum dhclient_state);
|
||
static unsigned int elapsed_in_this_state(const struct dhclient *cli);
|
||
static bool timeout(struct dhclient *, unsigned int secs);
|
||
|
||
static void dhclient_msg_init(struct dhclient *, enum dhcp_msg_type,
|
||
struct dhcp_msg *);
|
||
static void send_reliably(struct dhclient *cli,
|
||
void (*make_packet)(struct dhclient *,
|
||
struct dhcp_msg *));
|
||
static bool do_receive_msg(struct dhclient *, struct dhcp_msg *);
|
||
static void do_send_msg(struct dhclient *, const struct dhcp_msg *);
|
||
static bool receive_ack(struct dhclient *);
|
||
|
||
static unsigned int fuzz(unsigned int x, int max_fuzz);
|
||
static unsigned int calc_t2(unsigned int lease);
|
||
static unsigned int calc_t1(unsigned int lease, unsigned int t2);
|
||
|
||
static unsigned int clamp(unsigned int x, unsigned int min, unsigned int max);
|
||
|
||
/* Creates a new DHCP client to configure the network device 'netdev_name'
|
||
* (e.g. "eth0").
|
||
*
|
||
* If 'modify_request' is non-null, then each DHCP message to discover or
|
||
* request an address will be passed to it (along with auxiliary data 'aux').
|
||
* It may then add any desired options to the message for transmission.
|
||
*
|
||
* If 'validate_offer' is non-null, then each DHCP message that offers an
|
||
* address will be passed to it (along with auxiliary data 'aux') for
|
||
* validation: if it returns true, the address will accepted; otherwise, it
|
||
* will be rejected.
|
||
*
|
||
* The DHCP client will not start advertising for an IP address until
|
||
* dhclient_init() is called.
|
||
*
|
||
* If successful, returns 0 and sets '*cli' to the new DHCP client. Otherwise,
|
||
* returns a positive errno value and sets '*cli' to a null pointer. */
|
||
int
|
||
dhclient_create(const char *netdev_name,
|
||
void (*modify_request)(struct dhcp_msg *, void *aux),
|
||
bool (*validate_offer)(const struct dhcp_msg *, void *aux),
|
||
void *aux, struct dhclient **cli_)
|
||
{
|
||
struct dhclient *cli;
|
||
struct netdev_options netdev_options;
|
||
struct netdev *netdev;
|
||
int error;
|
||
|
||
*cli_ = NULL;
|
||
|
||
memset(&netdev_options, 0, sizeof netdev_options);
|
||
netdev_options.name = netdev_name;
|
||
netdev_options.ethertype = ETH_TYPE_IP;
|
||
netdev_options.may_create = true;
|
||
netdev_options.may_open = true;
|
||
|
||
error = netdev_open(&netdev_options, &netdev);
|
||
/* XXX install socket filter to catch only DHCP packets. */
|
||
if (error) {
|
||
VLOG_ERR("could not open %s network device: %s",
|
||
netdev_name, strerror(error));
|
||
return error;
|
||
}
|
||
|
||
error = netdev_turn_flags_on(netdev, NETDEV_UP, false);
|
||
if (error) {
|
||
VLOG_ERR("could not bring %s device up: %s",
|
||
netdev_name, strerror(error));
|
||
netdev_close(netdev);
|
||
return error;
|
||
}
|
||
|
||
cli = xzalloc(sizeof *cli);
|
||
cli->modify_request = modify_request;
|
||
cli->validate_offer = validate_offer;
|
||
cli->aux = aux;
|
||
cli->netdev = netdev;
|
||
cli->state = S_RELEASED;
|
||
cli->state_entered = time_now();
|
||
cli->xid = random_uint32();
|
||
cli->ipaddr = 0;
|
||
cli->server_ip = 0;
|
||
cli->retransmit = cli->delay = 0;
|
||
cli->max_timeout = 64;
|
||
cli->min_timeout = 1;
|
||
ds_init(&cli->s);
|
||
cli->changed = true;
|
||
*cli_ = cli;
|
||
return 0;
|
||
}
|
||
|
||
/* Sets the maximum amount of timeout that 'cli' will wait for a reply from
|
||
* the DHCP server before retransmitting, in seconds, to 'max_timeout'. The
|
||
* default is 64 seconds. */
|
||
void
|
||
dhclient_set_max_timeout(struct dhclient *cli, unsigned int max_timeout)
|
||
{
|
||
cli->max_timeout = MAX(2, max_timeout);
|
||
}
|
||
|
||
/* Destroys 'cli' and frees all related resources. */
|
||
void
|
||
dhclient_destroy(struct dhclient *cli)
|
||
{
|
||
if (cli) {
|
||
dhcp_msg_uninit(cli->binding);
|
||
free(cli->binding);
|
||
netdev_close(cli->netdev);
|
||
ds_destroy(&cli->s);
|
||
free(cli);
|
||
}
|
||
}
|
||
|
||
/* Returns the network device in use by 'cli'. The caller must not destroy
|
||
* the returned device. */
|
||
struct netdev *
|
||
dhclient_get_netdev(struct dhclient *cli)
|
||
{
|
||
return cli->netdev;
|
||
}
|
||
|
||
/* Forces 'cli' into a (re)initialization state, in which no address is bound
|
||
* but the client is advertising to obtain one. If 'requested_ip' is nonzero,
|
||
* then the client will attempt to re-bind to that IP address; otherwise, it
|
||
* will not ask for any particular address. */
|
||
void
|
||
dhclient_init(struct dhclient *cli, uint32_t requested_ip)
|
||
{
|
||
state_transition(cli, requested_ip ? S_INIT_REBOOT : S_INIT);
|
||
cli->ipaddr = requested_ip;
|
||
cli->min_timeout = 0;
|
||
cli->init_delay = 0;
|
||
}
|
||
|
||
/* Forces 'cli' to release its bound IP address (if any). The client will not
|
||
* advertise for a new address until dhclient_init() is called again. */
|
||
void
|
||
dhclient_release(struct dhclient *cli)
|
||
{
|
||
if (dhclient_is_bound(cli)) {
|
||
struct dhcp_msg msg;
|
||
dhclient_msg_init(cli, DHCPRELEASE, &msg);
|
||
msg.ciaddr = cli->ipaddr;
|
||
do_send_msg(cli, &msg);
|
||
dhcp_msg_uninit(&msg);
|
||
}
|
||
state_transition(cli, S_RELEASED);
|
||
cli->min_timeout = UINT_MAX;
|
||
}
|
||
|
||
static void
|
||
do_force_renew(struct dhclient *cli, int deadline)
|
||
{
|
||
time_t now = time_now();
|
||
unsigned int lease_left = sat_sub(cli->lease_expiration, now);
|
||
if (lease_left <= deadline) {
|
||
if (cli->state & (S_RENEWING | S_REBINDING)) {
|
||
return;
|
||
}
|
||
deadline = lease_left;
|
||
}
|
||
if (cli->state & (S_BOUND | S_RENEWING)) {
|
||
state_transition(cli, S_RENEWING);
|
||
cli->renewing_timeout = deadline * 3 / 4;
|
||
cli->rebinding_timeout = deadline * 1 / 4;
|
||
} else {
|
||
state_transition(cli, S_REBINDING);
|
||
cli->rebinding_timeout = deadline;
|
||
}
|
||
cli->min_timeout = 0;
|
||
}
|
||
|
||
/* Forces 'cli' to attempt to renew the lease its current IP address (if any)
|
||
* within 'deadline' seconds. If the deadline is not met, then the client
|
||
* gives up its IP address binding and re-starts the DHCP process. */
|
||
void
|
||
dhclient_force_renew(struct dhclient *cli, int deadline)
|
||
{
|
||
/* Drain the receive queue so that we know that any DHCPACK we process is
|
||
* freshly received. */
|
||
netdev_drain(cli->netdev);
|
||
|
||
switch (cli->state) {
|
||
case S_INIT:
|
||
case S_INIT_REBOOT:
|
||
case S_REBOOTING:
|
||
case S_SELECTING:
|
||
case S_REQUESTING:
|
||
break;
|
||
|
||
case S_BOUND:
|
||
case S_RENEWING:
|
||
case S_REBINDING:
|
||
do_force_renew(cli, deadline);
|
||
break;
|
||
|
||
case S_RELEASED:
|
||
dhclient_init(cli, 0);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* Returns true if 'cli' is bound to an IP address, false otherwise. */
|
||
bool
|
||
dhclient_is_bound(const struct dhclient *cli)
|
||
{
|
||
return cli->state & (S_BOUND | S_RENEWING | S_REBINDING);
|
||
}
|
||
|
||
/* Returns true if 'cli' has changed from bound to unbound, or vice versa, at
|
||
* least once since the last time this function was called. */
|
||
bool
|
||
dhclient_changed(struct dhclient *cli)
|
||
{
|
||
bool changed = cli->changed;
|
||
cli->changed = 0;
|
||
return changed;
|
||
}
|
||
|
||
/* Returns 'cli''s current state, as a string. The caller must not modify or
|
||
* free the string. */
|
||
const char *
|
||
dhclient_get_state(const struct dhclient *cli)
|
||
{
|
||
return state_name(cli->state);
|
||
}
|
||
|
||
/* Returns the number of seconds spent so far in 'cli''s current state. */
|
||
unsigned int
|
||
dhclient_get_state_elapsed(const struct dhclient *cli)
|
||
{
|
||
return elapsed_in_this_state(cli);
|
||
}
|
||
|
||
/* If 'cli' is bound, returns the number of seconds remaining in its lease;
|
||
* otherwise, returns 0. */
|
||
unsigned int
|
||
dhclient_get_lease_remaining(const struct dhclient *cli)
|
||
{
|
||
if (dhclient_is_bound(cli)) {
|
||
time_t now = time_now();
|
||
return cli->lease_expiration > now ? cli->lease_expiration - now : 0;
|
||
} else {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/* If 'cli' is bound to an IP address, returns that IP address; otherwise,
|
||
* returns 0. */
|
||
uint32_t
|
||
dhclient_get_ip(const struct dhclient *cli)
|
||
{
|
||
return dhclient_is_bound(cli) ? cli->ipaddr : 0;
|
||
}
|
||
|
||
/* If 'cli' is bound to an IP address, returns the netmask for that IP address;
|
||
* otherwise, returns 0. */
|
||
uint32_t
|
||
dhclient_get_netmask(const struct dhclient *cli)
|
||
{
|
||
return dhclient_is_bound(cli) ? cli->netmask : 0;
|
||
}
|
||
|
||
/* If 'cli' is bound to an IP address and 'cli' has a default gateway, returns
|
||
* that default gateway; otherwise, returns 0. */
|
||
uint32_t
|
||
dhclient_get_router(const struct dhclient *cli)
|
||
{
|
||
return dhclient_is_bound(cli) ? cli->router : 0;
|
||
}
|
||
|
||
/* If 'cli' is bound to an IP address, returns the DHCP message that was
|
||
* received to obtain that IP address (so that the caller can obtain additional
|
||
* options from it). Otherwise, returns a null pointer. */
|
||
const struct dhcp_msg *
|
||
dhclient_get_config(const struct dhclient *cli)
|
||
{
|
||
return dhclient_is_bound(cli) ? cli->binding : NULL;
|
||
}
|
||
|
||
/* Configures the network device backing 'cli' to the network address and other
|
||
* parameters obtained via DHCP. If no address is bound on 'cli', removes any
|
||
* configured address from 'cli'.
|
||
*
|
||
* To use a dhclient as a regular DHCP client that binds and unbinds from IP
|
||
* addresses in the usual fashion, call this function after dhclient_run() if
|
||
* anything has changed, like so:
|
||
*
|
||
* dhclient_run(cli);
|
||
* if (dhclient_changed(cli)) {
|
||
* dhclient_configure_netdev(cli);
|
||
* }
|
||
*
|
||
*/
|
||
int
|
||
dhclient_configure_netdev(struct dhclient *cli)
|
||
{
|
||
struct in_addr addr = { dhclient_get_ip(cli) };
|
||
struct in_addr mask = { dhclient_get_netmask(cli) };
|
||
struct in_addr router = { dhclient_get_router(cli) };
|
||
int error;
|
||
|
||
error = netdev_set_in4(cli->netdev, addr, mask);
|
||
if (error) {
|
||
VLOG_ERR("could not set %s address "IP_FMT"/"IP_FMT": %s",
|
||
netdev_get_name(cli->netdev),
|
||
IP_ARGS(&addr.s_addr), IP_ARGS(&mask.s_addr),
|
||
strerror(error));
|
||
}
|
||
|
||
if (!error && router.s_addr) {
|
||
error = netdev_add_router(cli->netdev, router);
|
||
if (error) {
|
||
VLOG_ERR("failed to add default route to "IP_FMT" on %s: %s",
|
||
IP_ARGS(&router), netdev_get_name(cli->netdev),
|
||
strerror(error));
|
||
}
|
||
}
|
||
|
||
return error;
|
||
}
|
||
|
||
/* If 'cli' is bound and the binding includes DNS domain parameters, updates
|
||
* /etc/resolv.conf will be updated to match the received parameters. Returns
|
||
* 0 if successful, otherwise a positive errno value. */
|
||
int
|
||
dhclient_update_resolv_conf(struct dhclient *cli)
|
||
{
|
||
uint32_t dns_server;
|
||
char *domain_name;
|
||
bool has_domain_name;
|
||
char new_name[128];
|
||
FILE *old, *new;
|
||
int i;
|
||
|
||
if (!dhclient_is_bound(cli)) {
|
||
return 0;
|
||
}
|
||
if (!dhcp_msg_get_ip(cli->binding, DHCP_CODE_DNS_SERVER, 0, &dns_server)) {
|
||
VLOG_DBG("binding does not include any DNS servers");
|
||
return 0;
|
||
}
|
||
|
||
sprintf(new_name, "/etc/resolv.conf.tmp%ld", (long int) getpid());
|
||
new = fopen(new_name, "w");
|
||
if (!new) {
|
||
VLOG_WARN("%s: create: %s", new_name, strerror(errno));
|
||
return errno;
|
||
}
|
||
|
||
domain_name = dhcp_msg_get_string(cli->binding, DHCP_CODE_DOMAIN_NAME);
|
||
has_domain_name = domain_name != NULL;
|
||
if (domain_name) {
|
||
if (strspn(domain_name, "-_.0123456789abcdefghijklmnopqrstuvwxyz"
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ") == strlen(domain_name)) {
|
||
fprintf(new, "domain %s\n", domain_name);
|
||
} else {
|
||
VLOG_WARN("ignoring invalid domain name %s", domain_name);
|
||
has_domain_name = false;
|
||
}
|
||
} else {
|
||
VLOG_DBG("binding does not include domain name");
|
||
}
|
||
free(domain_name);
|
||
|
||
for (i = 0; dhcp_msg_get_ip(cli->binding, DHCP_CODE_DNS_SERVER,
|
||
i, &dns_server); i++) {
|
||
fprintf(new, "nameserver "IP_FMT"\n", IP_ARGS(&dns_server));
|
||
}
|
||
|
||
old = fopen("/etc/resolv.conf", "r");
|
||
if (old) {
|
||
char line[128];
|
||
|
||
while (fgets(line, sizeof line, old)) {
|
||
char *kw = xmemdup0(line, strcspn(line, " \t\r\n"));
|
||
if (strcmp(kw, "nameserver")
|
||
&& (!has_domain_name
|
||
|| (strcmp(kw, "domain") && strcmp(kw, "search")))) {
|
||
fputs(line, new);
|
||
}
|
||
free(kw);
|
||
}
|
||
fclose(old);
|
||
} else {
|
||
VLOG_DBG("/etc/resolv.conf: open: %s", strerror(errno));
|
||
}
|
||
|
||
if (fclose(new) < 0) {
|
||
VLOG_WARN("%s: close: %s", new_name, strerror(errno));
|
||
return errno;
|
||
}
|
||
|
||
if (rename(new_name, "/etc/resolv.conf") < 0) {
|
||
VLOG_WARN("failed to rename %s to /etc/resolv.conf: %s",
|
||
new_name, strerror(errno));
|
||
return errno;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* DHCP protocol. */
|
||
|
||
static void
|
||
make_dhcpdiscover(struct dhclient *cli, struct dhcp_msg *msg)
|
||
{
|
||
cli->secs = elapsed_in_this_state(cli);
|
||
dhclient_msg_init(cli, DHCPDISCOVER, msg);
|
||
if (cli->ipaddr) {
|
||
dhcp_msg_put_ip(msg, DHCP_CODE_REQUESTED_IP, cli->ipaddr);
|
||
}
|
||
}
|
||
|
||
static void
|
||
make_dhcprequest(struct dhclient *cli, struct dhcp_msg *msg)
|
||
{
|
||
dhclient_msg_init(cli, DHCPREQUEST, msg);
|
||
msg->ciaddr = dhclient_get_ip(cli);
|
||
if (cli->state == S_REQUESTING) {
|
||
dhcp_msg_put_ip(msg, DHCP_CODE_SERVER_IDENTIFIER, cli->server_ip);
|
||
}
|
||
dhcp_msg_put_ip(msg, DHCP_CODE_REQUESTED_IP, cli->ipaddr);
|
||
}
|
||
|
||
static void
|
||
do_init(struct dhclient *cli, enum dhclient_state next_state)
|
||
{
|
||
if (!cli->init_delay) {
|
||
cli->init_delay = fuzz(2, 1);
|
||
}
|
||
if (timeout(cli, cli->init_delay)) {
|
||
state_transition(cli, next_state);
|
||
}
|
||
}
|
||
|
||
static void
|
||
dhclient_run_INIT(struct dhclient *cli)
|
||
{
|
||
do_init(cli, S_SELECTING);
|
||
}
|
||
|
||
static void
|
||
dhclient_run_INIT_REBOOT(struct dhclient *cli)
|
||
{
|
||
do_init(cli, S_REBOOTING);
|
||
}
|
||
|
||
static void
|
||
dhclient_run_REBOOTING(struct dhclient *cli)
|
||
{
|
||
send_reliably(cli, make_dhcprequest);
|
||
if (!receive_ack(cli) && timeout(cli, 60)) {
|
||
state_transition(cli, S_INIT);
|
||
}
|
||
}
|
||
|
||
static bool
|
||
dhcp_receive(struct dhclient *cli, unsigned int msgs, struct dhcp_msg *msg)
|
||
{
|
||
while (do_receive_msg(cli, msg)) {
|
||
if (msg->type > 31 || !((1u << msg->type) & msgs)) {
|
||
VLOG_DBG_RL(&rl, "received unexpected %s in %s state: %s",
|
||
dhcp_type_name(msg->type), state_name(cli->state),
|
||
dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else if (msg->xid != cli->xid) {
|
||
VLOG_DBG_RL(&rl,
|
||
"ignoring %s with xid != %08"PRIx32" in %s state: %s",
|
||
dhcp_type_name(msg->type), msg->xid,
|
||
state_name(cli->state),
|
||
dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else {
|
||
return true;
|
||
}
|
||
dhcp_msg_uninit(msg);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static bool
|
||
validate_offered_options(struct dhclient *cli, const struct dhcp_msg *msg)
|
||
{
|
||
uint32_t lease, netmask;
|
||
if (!dhcp_msg_get_secs(msg, DHCP_CODE_LEASE_TIME, 0, &lease)) {
|
||
VLOG_WARN_RL(&rl, "%s lacks lease time: %s", dhcp_type_name(msg->type),
|
||
dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else if (!dhcp_msg_get_ip(msg, DHCP_CODE_SUBNET_MASK, 0, &netmask)) {
|
||
VLOG_WARN_RL(&rl, "%s lacks netmask: %s", dhcp_type_name(msg->type),
|
||
dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else if (lease < MIN_ACCEPTABLE_LEASE) {
|
||
VLOG_WARN_RL(&rl, "Ignoring %s with %"PRIu32"-second lease time: %s",
|
||
dhcp_type_name(msg->type), lease,
|
||
dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else if (cli->validate_offer && !cli->validate_offer(msg, cli->aux)) {
|
||
VLOG_DBG_RL(&rl, "client validation hook refused offer: %s",
|
||
dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static void
|
||
dhclient_run_SELECTING(struct dhclient *cli)
|
||
{
|
||
struct dhcp_msg msg;
|
||
|
||
send_reliably(cli, make_dhcpdiscover);
|
||
if (cli->server_ip && timeout(cli, 60)) {
|
||
cli->server_ip = 0;
|
||
state_transition(cli, S_INIT);
|
||
}
|
||
for (; dhcp_receive(cli, 1u << DHCPOFFER, &msg); dhcp_msg_uninit(&msg)) {
|
||
if (!validate_offered_options(cli, &msg)) {
|
||
continue;
|
||
}
|
||
if (!dhcp_msg_get_ip(&msg, DHCP_CODE_SERVER_IDENTIFIER,
|
||
0, &cli->server_ip)) {
|
||
VLOG_WARN_RL(&rl, "DHCPOFFER lacks server identifier: %s",
|
||
dhcp_msg_to_string(&msg, false, &cli->s));
|
||
continue;
|
||
}
|
||
|
||
VLOG_DBG_RL(&rl, "accepting DHCPOFFER: %s",
|
||
dhcp_msg_to_string(&msg, false, &cli->s));
|
||
cli->ipaddr = msg.yiaddr;
|
||
state_transition(cli, S_REQUESTING);
|
||
break;
|
||
}
|
||
}
|
||
|
||
static bool
|
||
same_binding(const struct dhcp_msg *old, const struct dhcp_msg *new)
|
||
{
|
||
static const int codes[] = {
|
||
DHCP_CODE_SUBNET_MASK,
|
||
DHCP_CODE_ROUTER,
|
||
DHCP_CODE_DNS_SERVER,
|
||
DHCP_CODE_HOST_NAME,
|
||
DHCP_CODE_DOMAIN_NAME,
|
||
DHCP_CODE_IP_TTL,
|
||
DHCP_CODE_MTU,
|
||
DHCP_CODE_BROADCAST_ADDRESS,
|
||
DHCP_CODE_STATIC_ROUTE,
|
||
DHCP_CODE_ARP_CACHE_TIMEOUT,
|
||
DHCP_CODE_ETHERNET_ENCAPSULATION,
|
||
DHCP_CODE_TCP_TTL,
|
||
DHCP_CODE_SERVER_IDENTIFIER,
|
||
DHCP_CODE_OFP_CONTROLLER_VCONN,
|
||
DHCP_CODE_OFP_PKI_URI,
|
||
};
|
||
int i;
|
||
bool same = true;
|
||
|
||
if (old->yiaddr != new->yiaddr) {
|
||
VLOG_WARN("DHCP binding changed IP address from "IP_FMT" to "IP_FMT,
|
||
IP_ARGS(&old->yiaddr), IP_ARGS(&new->yiaddr));
|
||
same = false;
|
||
}
|
||
for (i = 0; i < ARRAY_SIZE(codes); i++) {
|
||
int code = codes[i];
|
||
const struct dhcp_option *old_opt = &old->options[code];
|
||
const struct dhcp_option *new_opt = &new->options[code];
|
||
if (!dhcp_option_equals(old_opt, new_opt)) {
|
||
struct ds old_string = DS_EMPTY_INITIALIZER;
|
||
struct ds new_string = DS_EMPTY_INITIALIZER;
|
||
VLOG_WARN("DHCP binding changed option from %s to %s",
|
||
dhcp_option_to_string(old_opt, code, &old_string),
|
||
dhcp_option_to_string(new_opt, code, &new_string));
|
||
ds_destroy(&old_string);
|
||
ds_destroy(&new_string);
|
||
same = false;
|
||
}
|
||
}
|
||
return same;
|
||
}
|
||
|
||
static bool
|
||
receive_ack(struct dhclient *cli)
|
||
{
|
||
struct dhcp_msg msg;
|
||
|
||
if (!dhcp_receive(cli, (1u << DHCPACK) | (1u << DHCPNAK), &msg)) {
|
||
return false;
|
||
} else if (msg.type == DHCPNAK) {
|
||
dhcp_msg_uninit(&msg);
|
||
state_transition(cli, S_INIT);
|
||
return true;
|
||
} else if (!validate_offered_options(cli, &msg)) {
|
||
dhcp_msg_uninit(&msg);
|
||
return false;
|
||
} else {
|
||
uint32_t lease = 0, t1 = 0, t2 = 0;
|
||
|
||
if (cli->binding) {
|
||
if (!same_binding(cli->binding, &msg)) {
|
||
cli->changed = true;
|
||
}
|
||
dhcp_msg_uninit(cli->binding);
|
||
} else {
|
||
cli->binding = xmalloc(sizeof *cli->binding);
|
||
}
|
||
dhcp_msg_copy(cli->binding, &msg);
|
||
|
||
dhcp_msg_get_secs(&msg, DHCP_CODE_LEASE_TIME, 0, &lease);
|
||
dhcp_msg_get_secs(&msg, DHCP_CODE_T1, 0, &t1);
|
||
dhcp_msg_get_secs(&msg, DHCP_CODE_T2, 0, &t2);
|
||
assert(lease >= MIN_ACCEPTABLE_LEASE);
|
||
|
||
if (!t2 || t2 >= lease) {
|
||
t2 = calc_t2(lease);
|
||
}
|
||
if (!t1 || t1 >= t2) {
|
||
t1 = calc_t1(lease, t2);
|
||
}
|
||
|
||
cli->lease_expiration = sat_add(time_now(), lease);
|
||
cli->bound_timeout = t1;
|
||
cli->renewing_timeout = t2 - t1;
|
||
cli->rebinding_timeout = lease - t2;
|
||
|
||
cli->ipaddr = msg.yiaddr;
|
||
dhcp_msg_get_ip(&msg, DHCP_CODE_SUBNET_MASK, 0, &cli->netmask);
|
||
if (!dhcp_msg_get_ip(&msg, DHCP_CODE_ROUTER, 0, &cli->router)) {
|
||
cli->router = INADDR_ANY;
|
||
}
|
||
state_transition(cli, S_BOUND);
|
||
VLOG_DBG("Bound: %s", dhcp_msg_to_string(&msg, false, &cli->s));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
static void
|
||
dhclient_run_REQUESTING(struct dhclient *cli)
|
||
{
|
||
send_reliably(cli, make_dhcprequest);
|
||
if (!receive_ack(cli) && timeout(cli, 60)) {
|
||
state_transition(cli, S_INIT);
|
||
}
|
||
}
|
||
|
||
static void
|
||
dhclient_run_BOUND(struct dhclient *cli)
|
||
{
|
||
if (timeout(cli, cli->bound_timeout)) {
|
||
state_transition(cli, S_RENEWING);
|
||
}
|
||
}
|
||
|
||
static void
|
||
dhclient_run_RENEWING(struct dhclient *cli)
|
||
{
|
||
send_reliably(cli, make_dhcprequest);
|
||
if (!receive_ack(cli) && timeout(cli, cli->renewing_timeout)) {
|
||
state_transition(cli, S_REBINDING);
|
||
}
|
||
}
|
||
|
||
static void
|
||
dhclient_run_REBINDING(struct dhclient *cli)
|
||
{
|
||
send_reliably(cli, make_dhcprequest);
|
||
if (!receive_ack(cli) && timeout(cli, cli->rebinding_timeout)) {
|
||
state_transition(cli, S_INIT);
|
||
}
|
||
}
|
||
|
||
static void
|
||
dhclient_run_RELEASED(struct dhclient *cli UNUSED)
|
||
{
|
||
/* Nothing to do. */
|
||
}
|
||
|
||
/* Processes the DHCP protocol for 'cli'. */
|
||
void
|
||
dhclient_run(struct dhclient *cli)
|
||
{
|
||
int old_state;
|
||
do {
|
||
old_state = cli->state;
|
||
cli->min_timeout = UINT_MAX;
|
||
cli->received = 0;
|
||
switch (cli->state) {
|
||
#define DHCLIENT_STATE(NAME, VALUE) \
|
||
case S_##NAME: dhclient_run_##NAME(cli); break;
|
||
DHCLIENT_STATES
|
||
#undef DHCLIENT_STATE
|
||
default:
|
||
NOT_REACHED();
|
||
}
|
||
} while (cli->state != old_state);
|
||
}
|
||
|
||
/* Sets up poll timeouts to wake up the poll loop when 'cli' needs to do some
|
||
* work. */
|
||
void
|
||
dhclient_wait(struct dhclient *cli)
|
||
{
|
||
if (cli->min_timeout != UINT_MAX) {
|
||
time_t now = time_now();
|
||
unsigned int wake = sat_add(cli->state_entered, cli->min_timeout);
|
||
if (wake <= now) {
|
||
poll_immediate_wake();
|
||
} else {
|
||
poll_timer_wait(sat_mul(sat_sub(wake, now), 1000));
|
||
}
|
||
}
|
||
/* Reset timeout to 1 second. This will have no effect ordinarily, because
|
||
* dhclient_run() will typically set it back to a higher value. If,
|
||
* however, the caller fails to call dhclient_run() before its next call to
|
||
* dhclient_wait() we won't potentially block forever. */
|
||
cli->min_timeout = 1;
|
||
|
||
if (cli->state & (S_SELECTING | S_REQUESTING | S_RENEWING | S_REBINDING)) {
|
||
netdev_recv_wait(cli->netdev);
|
||
}
|
||
}
|
||
|
||
static void
|
||
state_transition(struct dhclient *cli, enum dhclient_state state)
|
||
{
|
||
bool was_bound = dhclient_is_bound(cli);
|
||
bool am_bound;
|
||
if (cli->state != state) {
|
||
VLOG_DBG("entering %s", state_name(state));
|
||
cli->state = state;
|
||
}
|
||
cli->state_entered = time_now();
|
||
cli->retransmit = cli->delay = 0;
|
||
am_bound = dhclient_is_bound(cli);
|
||
if (was_bound != am_bound) {
|
||
cli->changed = true;
|
||
if (am_bound) {
|
||
assert(cli->binding != NULL);
|
||
VLOG_INFO("%s: obtained address "IP_FMT", netmask "IP_FMT,
|
||
netdev_get_name(cli->netdev),
|
||
IP_ARGS(&cli->ipaddr), IP_ARGS(&cli->netmask));
|
||
if (cli->router) {
|
||
VLOG_INFO("%s: obtained default gateway "IP_FMT,
|
||
netdev_get_name(cli->netdev), IP_ARGS(&cli->router));
|
||
}
|
||
} else {
|
||
dhcp_msg_uninit(cli->binding);
|
||
free(cli->binding);
|
||
cli->binding = NULL;
|
||
|
||
VLOG_INFO("%s: network address unbound",
|
||
netdev_get_name(cli->netdev));
|
||
}
|
||
}
|
||
if (cli->state & (S_SELECTING | S_REQUESTING | S_REBOOTING)) {
|
||
netdev_drain(cli->netdev);
|
||
}
|
||
}
|
||
|
||
static void
|
||
send_reliably(struct dhclient *cli,
|
||
void (*make_packet)(struct dhclient *, struct dhcp_msg *))
|
||
{
|
||
if (timeout(cli, cli->retransmit)) {
|
||
struct dhcp_msg msg;
|
||
make_packet(cli, &msg);
|
||
if (cli->modify_request) {
|
||
cli->modify_request(&msg, cli->aux);
|
||
}
|
||
do_send_msg(cli, &msg);
|
||
cli->delay = MIN(cli->max_timeout, MAX(4, cli->delay * 2));
|
||
cli->retransmit += fuzz(cli->delay, 1);
|
||
timeout(cli, cli->retransmit);
|
||
dhcp_msg_uninit(&msg);
|
||
}
|
||
}
|
||
|
||
static void
|
||
dhclient_msg_init(struct dhclient *cli, enum dhcp_msg_type type,
|
||
struct dhcp_msg *msg)
|
||
{
|
||
dhcp_msg_init(msg);
|
||
msg->op = DHCP_BOOTREQUEST;
|
||
msg->xid = cli->xid;
|
||
msg->secs = cli->secs;
|
||
msg->type = type;
|
||
netdev_get_etheraddr(cli->netdev, msg->chaddr);
|
||
}
|
||
|
||
/* If time goes backward this returns a large number, which makes it look like
|
||
* we've been in the current state a very long time. That's probably
|
||
* fine for that corner case--we'll just expire our lease, etc., and try to
|
||
* get a new one. */
|
||
static unsigned int
|
||
elapsed_in_this_state(const struct dhclient *cli)
|
||
{
|
||
return time_now() - cli->state_entered;
|
||
}
|
||
|
||
static bool
|
||
timeout(struct dhclient *cli, unsigned int secs)
|
||
{
|
||
cli->min_timeout = MIN(cli->min_timeout, secs);
|
||
return time_now() >= sat_add(cli->state_entered, secs);
|
||
}
|
||
|
||
static bool
|
||
do_receive_msg(struct dhclient *cli, struct dhcp_msg *msg)
|
||
{
|
||
uint8_t cli_mac[ETH_ADDR_LEN];
|
||
struct ofpbuf b;
|
||
int mtu;
|
||
|
||
netdev_get_mtu(cli->netdev, &mtu);
|
||
ofpbuf_init(&b, mtu + VLAN_ETH_HEADER_LEN);
|
||
netdev_get_etheraddr(cli->netdev, cli_mac);
|
||
for (; cli->received < 50; cli->received++) {
|
||
const struct ip_header *ip;
|
||
const struct dhcp_header *dhcp;
|
||
flow_t flow;
|
||
int error;
|
||
|
||
ofpbuf_clear(&b);
|
||
error = netdev_recv(cli->netdev, &b);
|
||
if (error) {
|
||
goto drained;
|
||
}
|
||
|
||
flow_extract(&b, 0, &flow);
|
||
if (flow.dl_type != htons(ETH_TYPE_IP)
|
||
|| flow.nw_proto != IP_TYPE_UDP
|
||
|| flow.tp_dst != htons(DHCP_CLIENT_PORT)
|
||
|| !(eth_addr_is_broadcast(flow.dl_dst)
|
||
|| eth_addr_equals(flow.dl_dst, cli_mac))) {
|
||
continue;
|
||
}
|
||
|
||
ip = b.l3;
|
||
if (IP_IS_FRAGMENT(ip->ip_frag_off)) {
|
||
/* We don't do reassembly. */
|
||
VLOG_WARN_RL(&rl, "ignoring fragmented DHCP datagram");
|
||
continue;
|
||
}
|
||
|
||
dhcp = b.l7;
|
||
if (!dhcp) {
|
||
VLOG_WARN_RL(&rl, "ignoring DHCP datagram with missing payload");
|
||
continue;
|
||
}
|
||
|
||
ofpbuf_pull(&b, (char *)b.l7 - (char*)b.data);
|
||
error = dhcp_parse(msg, &b);
|
||
if (!error) {
|
||
if (VLOG_IS_DBG_ENABLED()) {
|
||
VLOG_DBG_RL(&rl, "received %s",
|
||
dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else {
|
||
VLOG_INFO_RL(&rl, "received %s", dhcp_type_name(msg->type));
|
||
}
|
||
ofpbuf_uninit(&b);
|
||
return true;
|
||
}
|
||
}
|
||
netdev_drain(cli->netdev);
|
||
drained:
|
||
ofpbuf_uninit(&b);
|
||
return false;
|
||
}
|
||
|
||
static void
|
||
do_send_msg(struct dhclient *cli, const struct dhcp_msg *msg)
|
||
{
|
||
struct ofpbuf b;
|
||
struct eth_header eh;
|
||
struct ip_header nh;
|
||
struct udp_header th;
|
||
uint32_t udp_csum;
|
||
int error;
|
||
|
||
ofpbuf_init(&b, ETH_TOTAL_MAX);
|
||
ofpbuf_reserve(&b, ETH_HEADER_LEN + IP_HEADER_LEN + UDP_HEADER_LEN);
|
||
|
||
dhcp_assemble(msg, &b);
|
||
|
||
netdev_get_etheraddr(cli->netdev, eh.eth_src);
|
||
memcpy(eh.eth_dst, eth_addr_broadcast, ETH_ADDR_LEN);
|
||
eh.eth_type = htons(ETH_TYPE_IP);
|
||
|
||
nh.ip_ihl_ver = IP_IHL_VER(5, IP_VERSION);
|
||
nh.ip_tos = 0;
|
||
nh.ip_tot_len = htons(IP_HEADER_LEN + UDP_HEADER_LEN + b.size);
|
||
/* We can't guarantee uniqueness of ip_id versus the host's, screwing up
|
||
* fragment reassembly, so prevent fragmentation and use an all-zeros
|
||
* ip_id. RFC 791 doesn't say we can do this, but Linux does the same
|
||
* thing for DF packets, so it must not screw anything up. */
|
||
nh.ip_id = 0;
|
||
nh.ip_frag_off = htons(IP_DONT_FRAGMENT);
|
||
nh.ip_ttl = 64;
|
||
nh.ip_proto = IP_TYPE_UDP;
|
||
nh.ip_csum = 0;
|
||
nh.ip_src = dhclient_get_ip(cli);
|
||
/* XXX need to use UDP socket for nonzero server IPs so that we can get
|
||
* routing table support.
|
||
*
|
||
* if (...have server IP and in appropriate state...) {
|
||
* nh.ip_dst = cli->server_ip;
|
||
* } else {
|
||
* nh.ip_dst = INADDR_BROADCAST;
|
||
* }
|
||
*/
|
||
nh.ip_dst = INADDR_BROADCAST;
|
||
nh.ip_csum = csum(&nh, sizeof nh);
|
||
|
||
th.udp_src = htons(DHCP_CLIENT_PORT);
|
||
th.udp_dst = htons(DHCP_SERVER_PORT);
|
||
th.udp_len = htons(UDP_HEADER_LEN + b.size);
|
||
th.udp_csum = 0;
|
||
udp_csum = csum_add32(0, nh.ip_src);
|
||
udp_csum = csum_add32(udp_csum, nh.ip_dst);
|
||
udp_csum = csum_add16(udp_csum, IP_TYPE_UDP << 8);
|
||
udp_csum = csum_add16(udp_csum, th.udp_len);
|
||
udp_csum = csum_continue(udp_csum, &th, sizeof th);
|
||
th.udp_csum = csum_finish(csum_continue(udp_csum, b.data, b.size));
|
||
|
||
ofpbuf_push(&b, &th, sizeof th);
|
||
ofpbuf_push(&b, &nh, sizeof nh);
|
||
ofpbuf_push(&b, &eh, sizeof eh);
|
||
|
||
/* Don't try to send the frame if it's too long for an Ethernet frame. We
|
||
* disregard the network device's actual MTU because we don't want the
|
||
* frame to have to be discarded or fragmented if it travels over a regular
|
||
* Ethernet at some point. 1500 bytes should be enough for anyone. */
|
||
if (b.size <= ETH_TOTAL_MAX) {
|
||
if (VLOG_IS_DBG_ENABLED()) {
|
||
VLOG_DBG("sending %s", dhcp_msg_to_string(msg, false, &cli->s));
|
||
} else {
|
||
VLOG_INFO("sending %s", dhcp_type_name(msg->type));
|
||
}
|
||
error = netdev_send(cli->netdev, &b);
|
||
if (error) {
|
||
VLOG_ERR("send failed on %s: %s",
|
||
netdev_get_name(cli->netdev), strerror(error));
|
||
}
|
||
} else {
|
||
VLOG_ERR("cannot send %zu-byte Ethernet frame", b.size);
|
||
}
|
||
|
||
ofpbuf_uninit(&b);
|
||
}
|
||
|
||
static unsigned int
|
||
fuzz(unsigned int x, int max_fuzz)
|
||
{
|
||
/* Generate number in range [-max_fuzz, +max_fuzz]. */
|
||
int fuzz = random_range(max_fuzz * 2 + 1) - max_fuzz;
|
||
unsigned int y = x + fuzz;
|
||
return fuzz >= 0 ? (y >= x ? y : UINT_MAX) : (y <= x ? y : 0);
|
||
}
|
||
|
||
static unsigned int
|
||
clamp(unsigned int x, unsigned int min, unsigned int max)
|
||
{
|
||
return x < min ? min : x > max ? max : x;
|
||
}
|
||
|
||
static unsigned int
|
||
calc_t2(unsigned int lease)
|
||
{
|
||
unsigned int base = lease * 0.875;
|
||
return lease >= 60 ? clamp(fuzz(base, 10), 0, lease - 1) : base;
|
||
}
|
||
|
||
static unsigned int
|
||
calc_t1(unsigned int lease, unsigned int t2)
|
||
{
|
||
unsigned int base = lease / 2;
|
||
return lease >= 60 ? clamp(fuzz(base, 10), 0, t2 - 1) : base;
|
||
}
|