/*
* DHCPv4 Client Connection
*
* XXX
*/
#include <assert.h>
#include <c-stdaux.h>
#include <errno.h>
#include <limits.h>
#include <sys/socket.h> /* needed by linux/netdevice.h */
#include <linux/netdevice.h>
#include <net/if_arp.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include "n-dhcp4-private.h"
#include "util/packet.h"
/**
* n_dhcp4_c_connection_init() - initialize client connection
* @connection: connection to operate on
* @client_config: client configuration to use
* @probe_config: client probe configuration to use
* @log_queue: the log queue for logging events
* @fd_epoll: epoll context to attach to, or -1
*
* This initializes a new client connection using the configuration given in
* @client_config and @probe_config.
*
* The client-configuration given as @client_config must survive the lifetime
* of @connection. It is pinned in the connection and used all over the place.
* The caller must guarantee that the configuration is not deallocated in the
* meantime. Same is true for @probe_config.
*
* The new connection automatically attaches to the epoll context given as
* @fd_epoll. The epoll FD is retained in the connection and the caller must
* guarantee that it lives as long as the connection.
* The caller is explicitly allowed to pass -1 as @fd_epoll, in which case the
* connection will initialize correctly, but will not be in a usable state.
* That is, any call to n_dhcp4_c_connection_listen() will fail, since it will
* be unable to attach to the epoll context. Such a connection can be used to
* get a detached object that behaves sound, but provides no runtime.
*
* Return: 0 on success, negative error code on failure.
*/
int n_dhcp4_c_connection_init(NDhcp4CConnection *connection,
NDhcp4ClientConfig *client_config,
NDhcp4ClientProbeConfig *probe_config,
NDhcp4LogQueue *log_queue,
int fd_epoll) {
*connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection);
connection->client_config = client_config;
connection->probe_config = probe_config;
connection->fd_epoll = fd_epoll;
connection->log_queue = log_queue;
/*
* We explicitly allow initializing connections with an invalid
* epoll-fd. The resulting connection immediately transitions into the
* CLOSED state. This allows the caller to create dummy connections
* useful to provide asynchronous constructor-feedback in the API.
*
* The effect of this is as if you immediately call
* n_dhcp4_c_connection_close() on the new connection. However, by
* directly passing -1 in the constructor, you are guaranteed not even
* the constructor can ever mess with your epoll-set.
*/
if (connection->fd_epoll < 0)
connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED;
return 0;
}
/**
* n_dhcp4_c_connection_deinit() - deinitialize client connection
* @connection: connection to operate on
*
* This deinitializes a connection that was previously initialized via
* n_dhcp4_c_connection_init(). It will tear down all allocated state and
* release it.
*
* Once this function returns, @connection is re-initialized to
* N_DHCP4_C_CONNECTION_NULL. If this function is called on a deinitialized
* connection, it is a no-op.
*/
void n_dhcp4_c_connection_deinit(NDhcp4CConnection *connection) {
n_dhcp4_c_connection_close(connection);
n_dhcp4_outgoing_free(connection->request);
*connection = (NDhcp4CConnection)N_DHCP4_C_CONNECTION_NULL(*connection);
}
static void n_dhcp4_c_connection_outgoing_set_secs(NDhcp4Outgoing *message) {
uint64_t secs;
/*
* This function sets the `secs` field for outgoing messages. It
* expects the base-time and start-time to be already set by the
* caller.
* For a given outgoing message, its `secs` field describes the time
* (in seconds) between the start of the transaction this message is
* part of and the start of the operational process (also called the
* base time here).
*
* The operational process in the DHCP sense describes the entire
* process of requesting a lease and acquiring it. That is, it starts
* with the caller's intent to request a lease, and it ends when we
* got granted a lease. The act of refreshing a lease is, in itself, a
* new operational process. The base-time describes the start-time
* recorded when such a process as initiated.
*
* A transaction in the DHCP sense describes a request+reply
* combination, in most cases. That is, the time a request is sent is
* the start-time of a transaction. In the ideal case, the start-time
* of the first transaction in an operational process matches the
* base-time. However, transactions are often delayed with a randomized
* offset to reduce traffic during network bursts.
* In some cases, however, transactions are composed out of multiple
* requests+reply combinations. This includes, for instance, the SELECT
* message following an OFFER. The specification clearly says that
* those must be considered a single transaction and thus share the
* transaction start-time.
*
* The `secs` field, thus, describes how long a client has been busy
* requesting a lease. DHCP servers and proxies do use it to prioritize
* clients.
*
* Note: Some DHCP relays reject a `secs` value of 0 (which might look
* like it is uninitialized). Hence, we always clamp the value to
* the range `[1, 65535]`.
*/
secs = message->userdata.base_time - message->userdata.start_time;
secs /= 1000ULL * 1000ULL * 1000ULL; /* nsecs to secs */
secs = C_CLAMP(secs, 1, UINT16_MAX);
n_dhcp4_outgoing_set_secs(message, secs);
}
int n_dhcp4_c_connection_listen(NDhcp4CConnection *connection) {
_c_cleanup_(c_closep) int fd_packet = -1;
int r;
if (connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET)
return 0;
c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_INIT ||
connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING ||
connection->state == N_DHCP4_C_CONNECTION_STATE_UDP);
if (connection->fd_packet >= 0) {
epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
connection->fd_packet = c_close(connection->fd_packet);
}
if (connection->fd_udp >= 0) {
epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_udp, NULL);
connection->fd_udp = c_close(connection->fd_udp);
}
r = n_dhcp4_c_socket_packet_new(&fd_packet, connection->client_config->ifindex);
if (r)
return r;
r = epoll_ctl(connection->fd_epoll,
EPOLL_CTL_ADD,
fd_packet,
&(struct epoll_event){
.events = EPOLLIN,
.data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO },
});
if (r < 0)
return -errno;
connection->state = N_DHCP4_C_CONNECTION_STATE_PACKET;
connection->fd_packet = fd_packet;
fd_packet = -1;
return 0;
}
int n_dhcp4_c_connection_connect(NDhcp4CConnection *connection,
const struct in_addr *client,
const struct in_addr *server) {
int r, fd_udp;
c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET);
r = n_dhcp4_c_socket_udp_new(&fd_udp,
connection->client_config->ifindex,
client,
server);
if (r)
return r;
r = epoll_ctl(connection->fd_epoll,
EPOLL_CTL_ADD,
fd_udp,
&(struct epoll_event){
.events = EPOLLIN,
.data = { .u32 = N_DHCP4_CLIENT_EPOLL_IO },
});
if (r < 0) {
r = -errno;
goto exit_fd;
}
r = packet_shutdown(connection->fd_packet);
if (r < 0)
goto exit_epoll;
connection->state = N_DHCP4_C_CONNECTION_STATE_DRAINING;
connection->fd_udp = fd_udp;
connection->client_ip = client->s_addr;
connection->server_ip = server->s_addr;
fd_udp = -1;
return 0;
exit_epoll:
epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, fd_udp, NULL);
exit_fd:
close(fd_udp);
return r;
}
void n_dhcp4_c_connection_close(NDhcp4CConnection *connection) {
if (connection->fd_udp >= 0) {
epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_udp, NULL);
connection->fd_udp = c_close(connection->fd_udp);
}
if (connection->fd_packet >= 0) {
epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
connection->fd_packet = c_close(connection->fd_packet);
}
connection->fd_epoll = -1;
connection->state = N_DHCP4_C_CONNECTION_STATE_CLOSED;
}
static int n_dhcp4_c_connection_verify_incoming(NDhcp4CConnection *connection,
NDhcp4Incoming *message,
uint8_t *typep) {
NDhcp4Header *header = n_dhcp4_incoming_get_header(message);
uint8_t type;
uint32_t request_xid;
uint8_t *id;
size_t n_id;
int r;
r = n_dhcp4_incoming_query_message_type(message, &type);
if (r) {
if (r == N_DHCP4_E_UNSET)
return N_DHCP4_E_MALFORMED;
else
return r;
}
switch (type) {
case N_DHCP4_MESSAGE_OFFER:
case N_DHCP4_MESSAGE_ACK:
case N_DHCP4_MESSAGE_NAK:
/*
* Only accept replies if there is a pending request, and it
* has a matching transaction id.
*/
if (!connection->request)
return N_DHCP4_E_UNEXPECTED;
n_dhcp4_outgoing_get_xid(connection->request, &request_xid);
if (header->xid != request_xid)
return N_DHCP4_E_UNEXPECTED;
break;
case N_DHCP4_MESSAGE_FORCERENEW:
/*
* Force renew messages are triggered by a server, and do not
* match a pending request.
*/
break;
default:
return N_DHCP4_E_UNEXPECTED;
}
/*
* In case our transport makes use of the 'chaddr' field, make sure it
* matches exactly our address.
*/
switch (connection->client_config->transport) {
case N_DHCP4_TRANSPORT_ETHERNET:
c_assert(connection->client_config->n_mac == ETH_ALEN);
if (header->hlen != ETH_ALEN)
return N_DHCP4_E_UNEXPECTED;
if (memcmp(header->chaddr, connection->client_config->mac, ETH_ALEN) != 0)
return N_DHCP4_E_UNEXPECTED;
break;
case N_DHCP4_TRANSPORT_INFINIBAND:
if (header->hlen != 0)
return N_DHCP4_E_UNEXPECTED;
break;
}
/*
* If a server passes us back a client ID, it must be the one we
* provided. We ignore any packets that have mismatching client-ids.
*/
id = NULL;
n_id = 0;
r = n_dhcp4_incoming_query(message, N_DHCP4_OPTION_CLIENT_IDENTIFIER, &id, &n_id);
if (r) {
if (r != N_DHCP4_E_UNSET)
return r;
} else {
if (n_id != connection->client_config->n_client_id)
return N_DHCP4_E_UNEXPECTED;
if (memcmp(id, connection->client_config->client_id, n_id) != 0)
return N_DHCP4_E_UNEXPECTED;
}
*typep = type;
return 0;
}
void n_dhcp4_c_connection_get_timeout(NDhcp4CConnection *connection,
uint64_t *timeoutp) {
uint64_t timeout;
size_t n_send;
if (!connection->request) {
*timeoutp = 0;
return;
}
switch (connection->request->userdata.type) {
case N_DHCP4_C_MESSAGE_DISCOVER:
case N_DHCP4_C_MESSAGE_SELECT:
case N_DHCP4_C_MESSAGE_INFORM:
/*
* Resend with an exponential backoff and a one second random
* slack, from a minimum of two seconds to a maximum of sixty
* four.
*
* Note that the RFC says to start at four rather than two
* seconds, and use [-1,1] slack, rather than [0,1].
*/
n_send = connection->request->userdata.n_send;
if (n_send >= 6)
n_send = 6;
timeout = connection->request->userdata.send_time + ((1ULL << n_send) * 1000000000ULL) + connection->request->userdata.send_jitter;
break;
case N_DHCP4_C_MESSAGE_REBIND:
case N_DHCP4_C_MESSAGE_RENEW:
case N_DHCP4_C_MESSAGE_REBOOT:
/*
* Resend every sixty seconds with a one second random slack.
*
* Note that the RFC says to do this at most once, but we do
* it until we are cancelled.
*/
timeout = connection->request->userdata.send_time + (60ULL * 1000000000ULL) + connection->request->userdata.send_jitter;
break;
case N_DHCP4_C_MESSAGE_DECLINE:
case N_DHCP4_C_MESSAGE_RELEASE:
/* XXX make sure these message types are never pinned? */
timeout = 0;
break;
default:
c_assert(0);
}
*timeoutp = timeout;
}
static int n_dhcp4_c_connection_packet_broadcast(NDhcp4CConnection *connection,
NDhcp4Outgoing *message) {
int r;
c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_PACKET);
r = n_dhcp4_c_socket_packet_send(connection->fd_packet,
connection->client_config->ifindex,
connection->client_config->broadcast_mac,
connection->client_config->n_broadcast_mac,
message);
if (r)
return r;
return 0;
}
static int n_dhcp4_c_connection_udp_broadcast(NDhcp4CConnection *connection,
NDhcp4Outgoing *message) {
int r;
c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING ||
connection->state == N_DHCP4_C_CONNECTION_STATE_UDP);
r = n_dhcp4_c_socket_udp_broadcast(connection->fd_udp, message);
if (r)
return r;
return 0;
}
static int n_dhcp4_c_connection_udp_send(NDhcp4CConnection *connection,
NDhcp4Outgoing *message) {
int r;
c_assert(connection->state == N_DHCP4_C_CONNECTION_STATE_DRAINING ||
connection->state == N_DHCP4_C_CONNECTION_STATE_UDP);
r = n_dhcp4_c_socket_udp_send(connection->fd_udp, message);
if (r)
return r;
return 0;
}
static void n_dhcp4_c_connection_init_header(NDhcp4CConnection *connection,
NDhcp4Header *header) {
bool broadcast = connection->client_config->request_broadcast;
header->op = N_DHCP4_OP_BOOTREQUEST;
switch (connection->client_config->transport) {
case N_DHCP4_TRANSPORT_ETHERNET:
c_assert(connection->client_config->n_mac == ETH_ALEN);
header->htype = ARPHRD_ETHER;
header->hlen = ETH_ALEN;
memcpy(header->chaddr, connection->client_config->mac, ETH_ALEN);
break;
case N_DHCP4_TRANSPORT_INFINIBAND:
header->htype = ARPHRD_INFINIBAND;
header->hlen = 0;
/* infiniband mandates to request broadcasts */
broadcast = true;
break;
default:
abort();
break;
}
if (connection->client_ip != INADDR_ANY) {
header->ciaddr = connection->client_ip;
} else {
/*
* When the IP stack has not been configured, we may
* not be able to receive unicast packets, depending
* on the hardware. If that is the case we must request
* replies from the server to be broadcast.
*
* Once the IP stack has been configured, receiving
* unicast packets is never a problem, so the broadcast
* flag should not be set.
*/
if (broadcast)
header->flags |= N_DHCP4_MESSAGE_FLAG_BROADCAST;
}
}
static int n_dhcp4_c_connection_new_message(NDhcp4CConnection *connection,
NDhcp4Outgoing **messagep,
uint8_t type) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
NDhcp4Header *header;
uint8_t message_type;
bool via_packet_socket = false;
int r;
switch (type) {
case N_DHCP4_C_MESSAGE_DISCOVER:
message_type = N_DHCP4_MESSAGE_DISCOVER;
via_packet_socket = true;
break;
case N_DHCP4_C_MESSAGE_INFORM:
message_type = N_DHCP4_MESSAGE_INFORM;
break;
case N_DHCP4_C_MESSAGE_SELECT:
message_type = N_DHCP4_MESSAGE_REQUEST;
via_packet_socket = true;
break;
case N_DHCP4_C_MESSAGE_RENEW:
message_type = N_DHCP4_MESSAGE_REQUEST;
break;
case N_DHCP4_C_MESSAGE_REBIND:
message_type = N_DHCP4_MESSAGE_REQUEST;
break;
case N_DHCP4_C_MESSAGE_REBOOT:
message_type = N_DHCP4_MESSAGE_REQUEST;
via_packet_socket = true;
break;
case N_DHCP4_C_MESSAGE_RELEASE:
message_type = N_DHCP4_MESSAGE_RELEASE;
break;
case N_DHCP4_C_MESSAGE_DECLINE:
message_type = N_DHCP4_MESSAGE_DECLINE;
via_packet_socket = true;
break;
default:
abort();
return -ENOTRECOVERABLE;
}
/*
* We explicitly pass 0 as maximum message size, which makes
* NDhcp4Outgoing use the mandated default value from the spec (see its
* implementation). While the transport and like layers might support
* bigger MTUs (and we very likely know about them through
* n_dhcp4_client_update_mtu()), we cannot assume the target DHCP
* server supports parsing packets bigger than the minimum (and it is
* allowed to refuse bigger IP packets, even if the network supports
* transmission of them).
*
* We could theoretically increase this for packets other than the
* initial discovery. However, clients are unlikely to ever send large
* packets, so we just keep the same default for all outgoing packets.
*/
r = n_dhcp4_outgoing_new(&message, 0, N_DHCP4_OVERLOAD_FILE | N_DHCP4_OVERLOAD_SNAME);
if (r)
return r;
header = n_dhcp4_outgoing_get_header(message);
n_dhcp4_c_connection_init_header(connection, header);
message->userdata.type = type;
message->userdata.message_type = message_type;
/*
* Note that some implementations expect the MESSAGE_TYPE option to be
* the first option, and possibly even hard-code access to it. Hence,
* we really should make sure to pass it first as well.
*/
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MESSAGE_TYPE, &message_type, sizeof(message_type));
if (r)
return r;
r = n_dhcp4_outgoing_append(message,
N_DHCP4_OPTION_CLIENT_IDENTIFIER,
connection->client_config->client_id,
connection->client_config->n_client_id);
if (r)
return r;
switch (message_type) {
case N_DHCP4_MESSAGE_DISCOVER:
case N_DHCP4_MESSAGE_REQUEST:
case N_DHCP4_MESSAGE_INFORM: {
uint16_t mtu;
if (connection->probe_config->n_request_parameters > 0) {
r = n_dhcp4_outgoing_append(message,
N_DHCP4_OPTION_PARAMETER_REQUEST_LIST,
connection->probe_config->request_parameters,
connection->probe_config->n_request_parameters);
if (r)
return r;
}
if (via_packet_socket) {
/*
* In case of packet sockets, we do not support
* fragmentation. Hence, our maximum message size
* equals the transport MTU. In case no mtu is given,
* we use the minimum size mandated by the IP spec. If
* we omit the field, some implementations will
* interpret this to mean any packet size is supported,
* which we rather not want as default behavior (we can
* always support suppressing this field, if that is
* what the caller wants).
*/
mtu = htons(connection->mtu ?: N_DHCP4_NETWORK_IP_MINIMUM_MAX_SIZE);
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu));
if (r)
return r;
} else {
/*
* Once we use UDP sockets, we support fragmentation
* through the kernel IP stack. This means, the biggest
* message we can receive is the maximum UDP size plus
* the possible IP header. This would sum up to
* 2^16-1 + 20 (or even 2^16-1 + 60 if pedantic) and
* thus exceed the option field. Hence, we simply set
* the option to the maximum possible value.
*/
mtu = htons(UINT16_MAX);
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_MAXIMUM_MESSAGE_SIZE, &mtu, sizeof(mtu));
if (r)
return r;
}
break;
}
default:
break;
}
*messagep = message;
message = NULL;
return 0;
}
/*
* RFC2131 3.1
*
* The client broadcasts a DHCPDISCOVER message on its local physical
* subnet. The DHCPDISCOVER message MAY include options that suggest
* values for the network address and lease duration. BOOTP relay
* agents may pass the message on to DHCP servers not on the same
* physical subnet.
*
* RFC2131 3.5
*
* [...] in its initial DHCPDISCOVER or DHCPREQUEST message, a client
* may provide the server with a list of specific parameters the
* client is interested in. If the client includes a list of
* parameters in a DHCPDISCOVER message, it MUST include that list in
* any subsequent DHCPREQUEST messages.
*
* [...]
*
* In addition, the client may suggest values for the network address
* and lease time in the DHCPDISCOVER message. The client may include
* the 'requested IP address' option to suggest that a particular IP
* address be assigned, and may include the 'IP address lease time'
* option to suggest the lease time it would like. Other options
* representing "hints" at configuration parameters are allowed in a
* DHCPDISCOVER or DHCPREQUEST message.
*
* RFC2131 4.4.1
*
* The client generates and records a random transaction identifier and
* inserts that identifier into the 'xid' field. The client records its
* own local time for later use in computing the lease expiration. The
* client then broadcasts the DHCPDISCOVER on the local hardware
* broadcast address to the 0xffffffff IP broadcast address and 'DHCP
* server' UDP port.
*
* If the 'xid' of an arriving DHCPOFFER message does not match the
* 'xid' of the most recent DHCPDISCOVER message, the DHCPOFFER message
* must be silently discarded. Any arriving DHCPACK messages must be
* silently discarded.
*/
int n_dhcp4_c_connection_discover_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
int r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DISCOVER);
if (r)
return r;
*requestp = message;
message = NULL;
return 0;
}
/*
*
* RFC2131 4.1.1
*
* The DHCPREQUEST message contains the same 'xid' as the DHCPOFFER
* message.
*
* RFC2131 4.3.2
*
* Client inserts the address of the selected server in 'server
* identifier', 'ciaddr' MUST be zero, 'requested IP address' MUST be
* filled in with the yiaddr value from the chosen DHCPOFFER.
*/
int n_dhcp4_c_connection_select_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp,
NDhcp4Incoming *offer) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
struct in_addr client;
struct in_addr server;
uint32_t xid;
int r;
n_dhcp4_incoming_get_yiaddr(offer, &client);
r = n_dhcp4_incoming_query_server_identifier(offer, &server);
if (r)
return r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_SELECT);
if (r)
return r;
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client));
if (r)
return r;
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server));
if (r)
return r;
/*
* SELECT continues the transaction started by DISCOVER, and as such
* keeps the same start time. We also have to preserve the base time
* of the selected lease as well as the transaction ID.
*/
message->userdata.start_time = offer->userdata.start_time;
message->userdata.base_time = offer->userdata.base_time;
message->userdata.client_addr = client.s_addr;
n_dhcp4_incoming_get_xid(offer, &xid);
n_dhcp4_outgoing_set_xid(message, xid);
*requestp = message;
message = NULL;
return 0;
}
/*
* RFC2131 4.3.2
*
* 'server identifier' MUST NOT be filled in, 'requested IP address'
* option MUST be filled in with client's notion of its previously
* assigned address. 'ciaddr' MUST be zero. The client is seeking to
* verify a previously allocated, cached configuration. Server SHOULD
* send a DHCPNAK message to the client if the 'requested IP address'
* is incorrect, or is on the wrong network.
*/
int n_dhcp4_c_connection_reboot_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp,
const struct in_addr *client) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
int r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBOOT);
if (r)
return r;
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, client, sizeof(*client));
if (r)
return r;
*requestp = message;
message = NULL;
return 0;
}
/*
* RFC2131 4.3.2
*
* 'server identifier' MUST NOT be filled in, 'requested IP address'
* option MUST NOT be filled in, 'ciaddr' MUST be filled in with
* client's IP address. In this situation, the client is completely
* configured, and is trying to extend its lease. This message will
* be unicast, so no relay agents will be involved in its
* transmission. Because 'giaddr' is therefore not filled in, the
* DHCP server will trust the value in 'ciaddr', and use it when
* replying to the client.
*
* A client MAY choose to renew or extend its lease prior to T1. The
* server may choose not to extend the lease (as a policy decision by
* the network administrator), but should return a DHCPACK message
* regardless.
*
* RFC2131 4.4.5
*
* At time T1 the client moves to RENEWING state and sends (via unicast)
* a DHCPREQUEST message to the server to extend its lease. The client
* sets the 'ciaddr' field in the DHCPREQUEST to its current network
* address. The client records the local time at which the DHCPREQUEST
* message is sent for computation of the lease expiration time. The
* client MUST NOT include a 'server identifier' in the DHCPREQUEST
* message.
*/
int n_dhcp4_c_connection_renew_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
int r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RENEW);
if (r)
return r;
message->userdata.client_addr = connection->client_ip;
*requestp = message;
message = NULL;
return 0;
}
/*
* RFC2131 4.3.2
*
* 'server identifier' MUST NOT be filled in, 'requested IP address'
* option MUST NOT be filled in, 'ciaddr' MUST be filled in with
* client's IP address. In this situation, the client is completely
* configured, and is trying to extend its lease. This message MUST
* be broadcast to the 0xffffffff IP broadcast address. The DHCP
* server SHOULD check 'ciaddr' for correctness before replying to
* the DHCPREQUEST.
*
* RFC2131 4.4.5
*
* If no DHCPACK arrives before time T2, the client moves to REBINDING
* state and sends (via broadcast) a DHCPREQUEST message to extend its
* lease. The client sets the 'ciaddr' field in the DHCPREQUEST to its
* current network address. The client MUST NOT include a 'server
* identifier' in the DHCPREQUEST message.
*/
int n_dhcp4_c_connection_rebind_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
int r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_REBIND);
if (r)
return r;
message->userdata.client_addr = connection->client_ip;
*requestp = message;
message = NULL;
return 0;
}
/*
* RFC2131 3.2
*
* If the client detects that the IP address in the DHCPACK message
* is already in use, the client MUST send a DHCPDECLINE message to the
* server and restarts the configuration process by requesting a
* new network address.
*
* RFC2131 4.4.4
*
* Because the client is declining the use of the IP address supplied by
* the server, the client broadcasts DHCPDECLINE messages.
*/
int n_dhcp4_c_connection_decline_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp,
NDhcp4Incoming *ack,
const char *error) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
struct in_addr client;
struct in_addr server;
int r;
n_dhcp4_incoming_get_yiaddr(ack, &client);
r = n_dhcp4_incoming_query_server_identifier(ack, &server);
if (r)
return r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_DECLINE);
if (r)
return r;
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_REQUESTED_IP_ADDRESS, &client, sizeof(client));
if (r)
return r;
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &server, sizeof(server));
if (r)
return r;
if (error) {
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1);
if (r)
return r;
}
message->userdata.client_addr = client.s_addr;
*requestp = message;
message = NULL;
return 0;
}
/*
* RFC2131 3.4
*
* If a client has obtained a network address through some other means
* (e.g., manual configuration), it may use a DHCPINFORM request message
* to obtain other local configuration parameters.
*
* RFC2131 4.4
*
* The DHCPINFORM message is not shown in figure 5. A client simply
* sends the DHCPINFORM and waits for DHCPACK messages. Once the client
* has selected its parameters, it has completed the configuration
* process.
*
* RFC2131 4.4.3
*
* The client sends a DHCPINFORM message. The client may request
* specific configuration parameters by including the 'parameter request
* list' option. The client generates and records a random transaction
* identifier and inserts that identifier into the 'xid' field. The
* client places its own network address in the 'ciaddr' field. The
* client SHOULD NOT request lease time parameters.
*
* The client then unicasts the DHCPINFORM to the DHCP server if it
* knows the server's address, otherwise it broadcasts the message to
* the limited (all 1s) broadcast address. DHCPINFORM messages MUST be
* directed to the 'DHCP server' UDP port.
*/
int n_dhcp4_c_connection_inform_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
int r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_INFORM);
if (r)
return r;
message->userdata.client_addr = connection->client_ip;
*requestp = message;
message = NULL;
return 0;
}
/*
* RFC2131 3.1
*
* The client may choose to relinquish its lease on a network address
* by sending a DHCPRELEASE message to the server. The client
* identifies the lease to be released with its 'client identifier',
* or 'chaddr' and network address in the DHCPRELEASE message. If the
* client used a 'client identifier' when it obtained the lease, it
* MUST use the same 'client identifier' in the DHCPRELEASE message.
*
* RFC2131 3.2
*
* The client may choose to relinquish its lease on a network
* address by sending a DHCPRELEASE message to the server. The
* client identifies the lease to be released with its
* 'client identifier', or 'chaddr' and network address in the
* DHCPRELEASE message.
*
* Note that in this case, where the client retains its network
* address locally, the client will not normally relinquish its
* lease during a graceful shutdown. Only in the case where the
* client explicitly needs to relinquish its lease, e.g., the client
* is about to be moved to a different subnet, will the client send
* a DHCPRELEASE message.
*
* RFC2131 4.4.4
*
* The client unicasts DHCPRELEASE messages to the server.
*
* RFC2131 4.4.6
*
* If the client no longer requires use of its assigned network address
* (e.g., the client is gracefully shut down), the client sends a
* DHCPRELEASE message to the server. Note that the correct operation
* of DHCP does not depend on the transmission of DHCPRELEASE messages.
*/
int n_dhcp4_c_connection_release_new(NDhcp4CConnection *connection,
NDhcp4Outgoing **requestp,
const char *error) {
_c_cleanup_(n_dhcp4_outgoing_freep) NDhcp4Outgoing *message = NULL;
int r;
r = n_dhcp4_c_connection_new_message(connection, &message, N_DHCP4_C_MESSAGE_RELEASE);
if (r)
return r;
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_SERVER_IDENTIFIER, &connection->server_ip, sizeof(connection->server_ip));
if (r)
return r;
if (error) {
r = n_dhcp4_outgoing_append(message, N_DHCP4_OPTION_ERROR_MESSAGE, error, strlen(error) + 1);
if (r)
return r;
}
*requestp = message;
message = NULL;
return 0;
}
static const char *message_type_to_str(uint8_t type) {
switch (type) {
case N_DHCP4_MESSAGE_DISCOVER:
return "DISCOVER";
case N_DHCP4_MESSAGE_OFFER:
return "OFFER";
case N_DHCP4_MESSAGE_REQUEST:
return "REQUEST";
case N_DHCP4_MESSAGE_DECLINE:
return "DECLINE";
case N_DHCP4_MESSAGE_ACK:
return "ACK";
case N_DHCP4_MESSAGE_NAK:
return "NACK";
case N_DHCP4_MESSAGE_RELEASE:
return "RELEASE";
case N_DHCP4_MESSAGE_INFORM:
return "INFORM";
case N_DHCP4_MESSAGE_FORCERENEW:
return "FORCERENEW";
default:
return "UNKNOWN";
}
}
static int n_dhcp4_c_connection_send_request(NDhcp4CConnection *connection,
NDhcp4Outgoing *request,
uint64_t timestamp) {
char server_addr[INET_ADDRSTRLEN];
char client_addr[INET_ADDRSTRLEN];
char error_msg[128];
int r;
bool broadcast = false;
/*
* Increment the base time and reset the xid field,
* where applicable. We never alter the header on
* resends of SELECT, as it must always match the
* OFFER message they are in reply to.
*/
switch (request->userdata.type) {
case N_DHCP4_C_MESSAGE_DISCOVER:
case N_DHCP4_C_MESSAGE_INFORM:
case N_DHCP4_C_MESSAGE_REBOOT:
case N_DHCP4_C_MESSAGE_REBIND:
case N_DHCP4_C_MESSAGE_RENEW:
request->userdata.base_time = timestamp;
n_dhcp4_outgoing_set_xid(request, n_dhcp4_client_probe_config_get_random(connection->probe_config));
break;
case N_DHCP4_C_MESSAGE_SELECT:
case N_DHCP4_C_MESSAGE_DECLINE:
case N_DHCP4_C_MESSAGE_RELEASE:
break;
default:
c_assert(0);
}
request->userdata.send_time = timestamp;
request->userdata.send_jitter = (n_dhcp4_client_probe_config_get_random(connection->probe_config) % 1000000000ULL);
n_dhcp4_c_connection_outgoing_set_secs(request);
switch (request->userdata.type) {
case N_DHCP4_C_MESSAGE_DISCOVER:
case N_DHCP4_C_MESSAGE_SELECT:
case N_DHCP4_C_MESSAGE_REBOOT:
case N_DHCP4_C_MESSAGE_DECLINE:
case N_DHCP4_C_MESSAGE_REBIND:
broadcast = true;
r = n_dhcp4_c_connection_packet_broadcast(connection, request);
break;
case N_DHCP4_C_MESSAGE_INFORM:
broadcast = true;
r = n_dhcp4_c_connection_udp_broadcast(connection, request);
break;
case N_DHCP4_C_MESSAGE_RENEW:
case N_DHCP4_C_MESSAGE_RELEASE:
r = n_dhcp4_c_connection_udp_send(connection, request);
break;
default:
c_assert(0);
}
if (r) {
snprintf(error_msg, sizeof(error_msg), ": error %d", r);
} else {
error_msg[0] = '\0';
}
if (request->userdata.client_addr == INADDR_ANY) {
n_dhcp4_log(connection->log_queue,
LOG_INFO,
"send %s to %s%s",
message_type_to_str(request->userdata.message_type),
broadcast ?
"255.255.255.255" :
inet_ntop(AF_INET, &connection->server_ip,
server_addr, sizeof(server_addr)),
error_msg);
} else {
n_dhcp4_log(connection->log_queue,
LOG_INFO,
"send %s of %s to %s%s",
message_type_to_str(request->userdata.message_type),
inet_ntop(AF_INET, &request->userdata.client_addr,
client_addr, sizeof(client_addr)),
broadcast ?
"255.255.255.255" :
inet_ntop(AF_INET, &connection->server_ip,
server_addr, sizeof(server_addr)),
error_msg);
}
++request->userdata.n_send;
return 0;
}
int n_dhcp4_c_connection_start_request(NDhcp4CConnection *connection,
NDhcp4Outgoing *request,
uint64_t timestamp) {
int r;
/*
* This function starts a request, but in the case of SELECT it
* continues a previous transaction, so we do not want to reset
* the start time. Only set the start time if it was not already
* set.
*/
if (request->userdata.start_time == 0)
request->userdata.start_time = timestamp;
connection->request = n_dhcp4_outgoing_free(connection->request);
r = n_dhcp4_c_connection_send_request(connection, request, timestamp);
if (r)
return r;
connection->request = request;
return 0;
}
int n_dhcp4_c_connection_dispatch_timer(NDhcp4CConnection *connection,
uint64_t timestamp) {
uint64_t timeout;
int r;
if (!connection->request)
return 0;
n_dhcp4_c_connection_get_timeout(connection, &timeout);
if (timeout > timestamp)
return 0;
r = n_dhcp4_c_connection_send_request(connection, connection->request, timestamp);
if (r)
return r;
return 0;
}
/*
* Returns:
* 0 on success
* N_DHCP4_E_MALFORMED if a malformed packet was received
* N_DHCP4_E_UNEXPECTED if the packet received contains unexpected data
* N_DHCP4_E_AGAIN if there was another error (non fatal for the client)
*/
int n_dhcp4_c_connection_dispatch_io(NDhcp4CConnection *connection,
NDhcp4Incoming **messagep) {
_c_cleanup_(n_dhcp4_incoming_freep) NDhcp4Incoming *message = NULL;
char serv_addr[INET_ADDRSTRLEN];
char client_addr[INET_ADDRSTRLEN];
uint8_t type;
int r;
switch (connection->state) {
case N_DHCP4_C_CONNECTION_STATE_PACKET:
r = n_dhcp4_c_socket_packet_recv(connection->fd_packet,
connection->scratch_buffer,
sizeof(connection->scratch_buffer),
&message);
if (!r)
break;
else if (r == N_DHCP4_E_MALFORMED)
return r;
return N_DHCP4_E_AGAIN;
case N_DHCP4_C_CONNECTION_STATE_DRAINING:
r = n_dhcp4_c_socket_packet_recv(connection->fd_packet,
connection->scratch_buffer,
sizeof(connection->scratch_buffer),
&message);
if (!r)
break;
else if (r == N_DHCP4_E_MALFORMED)
return r;
else if (r != N_DHCP4_E_AGAIN)
return N_DHCP4_E_AGAIN;
/*
* The UDP socket is open and the packet socket has been shut down
* and drained, clean up the packet socket and fall through to
* dispatching the UDP socket.
*/
r = epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
c_assert(!r);
connection->fd_packet = c_close(connection->fd_packet);
connection->state = N_DHCP4_C_CONNECTION_STATE_UDP;
/* fall-through */
case N_DHCP4_C_CONNECTION_STATE_UDP:
r = n_dhcp4_c_socket_udp_recv(connection->fd_udp,
connection->scratch_buffer,
sizeof(connection->scratch_buffer),
&message);
if (!r)
break;
else if (r == N_DHCP4_E_MALFORMED)
return r;
return N_DHCP4_E_AGAIN;
default:
abort();
return -ENOTRECOVERABLE;
}
r = n_dhcp4_c_connection_verify_incoming(connection, message, &type);
if (r == N_DHCP4_E_MALFORMED || r == N_DHCP4_E_UNEXPECTED)
return r;
else if (r != 0)
return N_DHCP4_E_AGAIN;
if (type == N_DHCP4_MESSAGE_OFFER || type == N_DHCP4_MESSAGE_ACK) {
n_dhcp4_log(connection->log_queue,
LOG_INFO,
"received %s of %s from %s",
message_type_to_str(type),
inet_ntop(AF_INET, &message->message.header.yiaddr,
client_addr, sizeof(client_addr)),
inet_ntop(AF_INET, &message->message.header.siaddr,
serv_addr, sizeof(serv_addr)));
} else {
n_dhcp4_log(connection->log_queue,
LOG_INFO,
"received %s from %s",
message_type_to_str(type),
inet_ntop(AF_INET, &message->message.header.siaddr,
serv_addr, sizeof(serv_addr)));
}
switch (type) {
case N_DHCP4_MESSAGE_OFFER:
case N_DHCP4_MESSAGE_ACK:
case N_DHCP4_MESSAGE_NAK:
/*
* Remember the start time of the transaction, and the base
* time of any relative timestamps from the pending request.
* Thes same times applies to the response, and sholud be
* copied over.
*/
message->userdata.start_time = connection->request->userdata.start_time;
message->userdata.base_time = connection->request->userdata.base_time;
if (type != N_DHCP4_MESSAGE_OFFER) {
/*
* We only allow one reply to ACK or NAK, but for OFFER we must
* accept several, so we do not free the pinned request.
*/
connection->request = n_dhcp4_outgoing_free(connection->request);
}
break;
default:
break;
}
*messagep = message;
message = NULL;
return 0;
}