/*
 * 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);
                connection->ns_drain_timeout = 0;
        }

        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) {
        _c_cleanup_(c_closep) int fd_udp = -1;
        int r;

        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,
                                     connection->probe_config->dscp);
        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)
                return -errno;

        r = packet_shutdown(connection->fd_packet);
        if (r < 0) {
                epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, fd_udp, NULL);
                return r;
        }

        connection->state = N_DHCP4_C_CONNECTION_STATE_DRAINING;
        connection->ns_drain_timeout = n_dhcp4_gettime(CLOCK_BOOTTIME) + UINT64_C(10000000000);
        connection->fd_udp = fd_udp;
        fd_udp = -1;
        connection->client_ip = client->s_addr;
        connection->server_ip = server->s_addr;
        return 0;
}

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;
        connection->ns_drain_timeout = 0;
}

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 = connection->ns_drain_timeout;
                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);
        }

        if (connection->ns_drain_timeout != 0 && connection->ns_drain_timeout < timeout)
            timeout = connection->ns_drain_timeout;

        *timeoutp = timeout;
}

void n_dhcp4_c_connection_clear_client_ip(NDhcp4CConnection *connection) {
        connection->client_ip = INADDR_ANY;
}

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,
                                         connection->probe_config->dscp,
                                         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,
                                             uint8_t type) {
        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;
        }

        /*
         * Set ciaddr according to RFC2131 section 4:
         * - must be zero for initial messages (DISCOVER, SELECT)
         * - must be zero for REBOOT and DECLINE
         * - must be client's IP for all other messages (RENEW, REBIND, RELEASE, INFORM)
         */
        switch (type) {
        case N_DHCP4_C_MESSAGE_DISCOVER:    /* 4.4.1 - must be zero */
        case N_DHCP4_C_MESSAGE_SELECT:      /* 4.3.2 - must be zero in SELECTING */
        case N_DHCP4_C_MESSAGE_REBOOT:      /* 4.3.2 - must be zero in INIT-REBOOT */
        case N_DHCP4_C_MESSAGE_DECLINE:     /* 4.4   - must be zero per RFC2131 table */
                header->ciaddr = INADDR_ANY;
                break;
        default:
                /* All other message types must use client's current IP */
                header->ciaddr = connection->client_ip;
                break;
        }

        /*
         * 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 (connection->client_ip == INADDR_ANY && 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, type);

        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;

        if (connection->client_config->n_client_id > 0) {
                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;
        message->userdata.server_id = server.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";
        }
}

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:
        case N_DHCP4_C_MESSAGE_DECLINE:
        case N_DHCP4_C_MESSAGE_RELEASE:
                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:
                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->ns_drain_timeout != 0 && connection->ns_drain_timeout < timestamp) {
                epoll_ctl(connection->fd_epoll, EPOLL_CTL_DEL, connection->fd_packet, NULL);
                connection->fd_packet = c_close(connection->fd_packet);
                connection->state = N_DHCP4_C_CONNECTION_STATE_UDP;
                connection->ns_drain_timeout = 0;
        }

        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;
        _c_cleanup_(c_freep) uint8_t *buffer = NULL;
        char serv_addr[INET_ADDRSTRLEN];
        char client_addr[INET_ADDRSTRLEN];
        uint8_t type = 0;
        int r;

        buffer = malloc(UINT16_MAX);
        if (!buffer)
                return -ENOMEM;

        switch (connection->state) {
        case N_DHCP4_C_CONNECTION_STATE_PACKET:
                r = n_dhcp4_c_socket_packet_recv(connection->fd_packet,
                                                 buffer,
                                                 UINT16_MAX,
                                                 &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,
                                                 buffer,
                                                 UINT16_MAX,
                                                 &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;
                connection->ns_drain_timeout = 0;

                /* fall-through */
        case N_DHCP4_C_CONNECTION_STATE_UDP:
                r = n_dhcp4_c_socket_udp_recv(connection->fd_udp,
                                              buffer,
                                              UINT16_MAX,
                                              &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)));
        }

        if (type == N_DHCP4_MESSAGE_NAK &&
            connection->request->userdata.server_id != INADDR_ANY) {
                struct in_addr server;

                r = n_dhcp4_incoming_query_server_identifier(message, &server);
                if (r)
                        return N_DHCP4_E_AGAIN;

                if (connection->request->userdata.server_id != server.s_addr) {
                        n_dhcp4_log(connection->log_queue,
                                    LOG_DEBUG,
                                    "discarded NAK with wrong server-id %s",
                                    inet_ntop(AF_INET, &server,
                                              serv_addr, sizeof(serv_addr)));
                        return N_DHCP4_E_AGAIN;
                }
        }

        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.
                 * The same time applies to the response, and should 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;
}
