chiark / gitweb /
treewide: correct typos and use consistent "MAC" spelling
[elogind.git] / src / libsystemd-network / sd-dhcp-server.c
index 885d68d33594ad960b01bb72f4d74d705cca6ed6..3f89f344d74905c2ebe87a8f10697f01b1515ed5 100644 (file)
   along with systemd; If not, see <http://www.gnu.org/licenses/>.
 ***/
 
+#include <sys/ioctl.h>
+#include <netinet/if_ether.h>
+
+#include "siphash24.h"
+
 #include "sd-dhcp-server.h"
 #include "dhcp-server-internal.h"
+#include "dhcp-internal.h"
+
+#define DHCP_DEFAULT_LEASE_TIME         3600 /* one hour */
+
+int sd_dhcp_server_set_lease_pool(sd_dhcp_server *server,
+                                  struct in_addr *address,
+                                  size_t size) {
+        assert_return(server, -EINVAL);
+        assert_return(address, -EINVAL);
+        assert_return(address->s_addr, -EINVAL);
+        assert_return(size, -EINVAL);
+        assert_return(server->pool_start == htobe32(INADDR_ANY), -EBUSY);
+        assert_return(!server->pool_size, -EBUSY);
+        assert_return(!server->bound_leases, -EBUSY);
+
+        server->bound_leases = new0(DHCPLease*, size);
+        if (!server->bound_leases)
+                return -ENOMEM;
+
+        server->pool_start = address->s_addr;
+        server->pool_size = size;
+
+        return 0;
+}
+
+int sd_dhcp_server_set_address(sd_dhcp_server *server, struct in_addr *address,
+                               unsigned char prefixlen) {
+        assert_return(server, -EINVAL);
+        assert_return(address, -EINVAL);
+        assert_return(address->s_addr, -EINVAL);
+        assert_return(prefixlen <= 32, -ERANGE);
+        assert_return(server->address == htobe32(INADDR_ANY), -EBUSY);
+        assert_return(server->netmask == htobe32(INADDR_ANY), -EBUSY);
+
+        server->address = address->s_addr;
+        server->netmask = htobe32(0xfffffffflu << (32 - prefixlen));
+
+        return 0;
+}
+
+bool sd_dhcp_server_is_running(sd_dhcp_server *server) {
+        assert_return(server, -EINVAL);
+
+        return !!server->receive_message;
+}
 
 sd_dhcp_server *sd_dhcp_server_ref(sd_dhcp_server *server) {
         if (server)
@@ -30,27 +80,90 @@ sd_dhcp_server *sd_dhcp_server_ref(sd_dhcp_server *server) {
         return server;
 }
 
+unsigned long client_id_hash_func(const void *p,
+                                  const uint8_t hash_key[HASH_KEY_SIZE]) {
+        uint64_t u;
+        const DHCPClientId *id = p;
+
+        assert(id);
+        assert(id->length);
+        assert(id->data);
+
+        siphash24((uint8_t*) &u, id->data, id->length, hash_key);
+
+        return (unsigned long) u;
+}
+
+int client_id_compare_func(const void *_a, const void *_b) {
+        const DHCPClientId *a, *b;
+
+        a = _a;
+        b = _b;
+
+        assert(!a->length || a->data);
+        assert(!b->length || b->data);
+
+        if (a->length != b->length)
+                return a->length < b->length ? -1 : 1;
+
+        return memcmp(a->data, b->data, a->length);
+}
+
+static const struct hash_ops client_id_hash_ops = {
+        .hash = client_id_hash_func,
+        .compare = client_id_compare_func
+};
+
+static void dhcp_lease_free(DHCPLease *lease) {
+        if (!lease)
+                return;
+
+        free(lease->client_id.data);
+        free(lease);
+}
+
 sd_dhcp_server *sd_dhcp_server_unref(sd_dhcp_server *server) {
-        if (server && REFCNT_DEC(server->n_ref) <= 0) {
-                log_dhcp_server(server, "UNREF");
+        DHCPLease *lease;
 
-                sd_event_unref(server->event);
-                free(server);
-        }
+        if (!server)
+                return NULL;
+
+        if (REFCNT_DEC(server->n_ref) > 0)
+                return NULL;
+
+        log_dhcp_server(server, "UNREF");
+
+        sd_dhcp_server_stop(server);
+
+        sd_event_unref(server->event);
+
+        while ((lease = hashmap_steal_first(server->leases_by_client_id)))
+                dhcp_lease_free(lease);
+        hashmap_free(server->leases_by_client_id);
+
+        free(server->bound_leases);
+        free(server);
 
         return NULL;
 }
 
-int sd_dhcp_server_new(sd_dhcp_server **ret) {
+int sd_dhcp_server_new(sd_dhcp_server **ret, int ifindex) {
         _cleanup_dhcp_server_unref_ sd_dhcp_server *server = NULL;
 
         assert_return(ret, -EINVAL);
+        assert_return(ifindex > 0, -EINVAL);
 
         server = new0(sd_dhcp_server, 1);
         if (!server)
                 return -ENOMEM;
 
         server->n_ref = REFCNT_INIT;
+        server->fd_raw = -1;
+        server->fd = -1;
+        server->address = htobe32(INADDR_ANY);
+        server->netmask = htobe32(INADDR_ANY);
+        server->index = ifindex;
+        server->leases_by_client_id = hashmap_new(&client_id_hash_ops);
 
         *ret = server;
         server = NULL;
@@ -58,7 +171,8 @@ int sd_dhcp_server_new(sd_dhcp_server **ret) {
         return 0;
 }
 
-int sd_dhcp_server_attach_event(sd_dhcp_server *server, sd_event *event, int priority) {
+int sd_dhcp_server_attach_event(sd_dhcp_server *server, sd_event *event,
+                                int priority) {
         int r;
 
         assert_return(server, -EINVAL);
@@ -90,3 +204,789 @@ sd_event *sd_dhcp_server_get_event(sd_dhcp_server *server) {
 
         return server->event;
 }
+
+int sd_dhcp_server_stop(sd_dhcp_server *server) {
+        assert_return(server, -EINVAL);
+
+        server->receive_message =
+                sd_event_source_unref(server->receive_message);
+
+        server->fd_raw = safe_close(server->fd_raw);
+        server->fd = safe_close(server->fd);
+
+        log_dhcp_server(server, "STOPPED");
+
+        return 0;
+}
+
+static int dhcp_server_send_unicast_raw(sd_dhcp_server *server,
+                                        DHCPPacket *packet, size_t len) {
+        union sockaddr_union link = {
+                .ll.sll_family = AF_PACKET,
+                .ll.sll_protocol = htons(ETH_P_IP),
+                .ll.sll_ifindex = server->index,
+                .ll.sll_halen = ETH_ALEN,
+        };
+        int r;
+
+        assert(server);
+        assert(server->index > 0);
+        assert(server->address);
+        assert(packet);
+        assert(len > sizeof(DHCPPacket));
+
+        memcpy(&link.ll.sll_addr, &packet->dhcp.chaddr, ETH_ALEN);
+
+        dhcp_packet_append_ip_headers(packet, server->address, DHCP_PORT_SERVER,
+                                      packet->dhcp.yiaddr,
+                                      DHCP_PORT_CLIENT, len);
+
+        r = dhcp_network_send_raw_socket(server->fd_raw, &link, packet, len);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination,
+                                DHCPMessage *message, size_t len) {
+        union sockaddr_union dest = {
+                .in.sin_family = AF_INET,
+                .in.sin_port = htobe16(DHCP_PORT_CLIENT),
+                .in.sin_addr.s_addr = destination,
+        };
+        struct iovec iov = {
+                .iov_base = message,
+                .iov_len = len,
+        };
+        uint8_t cmsgbuf[CMSG_LEN(sizeof(struct in_pktinfo))] = {};
+        struct msghdr msg = {
+                .msg_name = &dest,
+                .msg_namelen = sizeof(dest.in),
+                .msg_iov = &iov,
+                .msg_iovlen = 1,
+                .msg_control = cmsgbuf,
+                .msg_controllen = sizeof(cmsgbuf),
+        };
+        struct cmsghdr *cmsg;
+        struct in_pktinfo *pktinfo;
+        int r;
+
+        assert(server);
+        assert(server->fd > 0);
+        assert(message);
+        assert(len > sizeof(DHCPMessage));
+
+        cmsg = CMSG_FIRSTHDR(&msg);
+        assert(cmsg);
+
+        cmsg->cmsg_level = IPPROTO_IP;
+        cmsg->cmsg_type = IP_PKTINFO;
+        cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
+
+        /* we attach source interface and address info to the message
+           rather than binding the socket. This will be mostly useful
+           when we gain support for arbitrary number of server addresses
+         */
+        pktinfo = (struct in_pktinfo*) CMSG_DATA(cmsg);
+        assert(pktinfo);
+
+        pktinfo->ipi_ifindex = server->index;
+        pktinfo->ipi_spec_dst.s_addr = server->address;
+
+        r = sendmsg(server->fd, &msg, 0);
+        if (r < 0)
+                return -errno;
+
+        return 0;
+}
+
+static bool requested_broadcast(DHCPRequest *req) {
+        assert(req);
+
+        return req->message->flags & htobe16(0x8000);
+}
+
+int dhcp_server_send_packet(sd_dhcp_server *server,
+                            DHCPRequest *req, DHCPPacket *packet,
+                            int type, size_t optoffset) {
+        be32_t destination = INADDR_ANY;
+        int r;
+
+        assert(server);
+        assert(req);
+        assert(req->max_optlen);
+        assert(optoffset <= req->max_optlen);
+        assert(packet);
+
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0,
+                               DHCP_OPTION_SERVER_IDENTIFIER,
+                               4, &server->address);
+        if (r < 0)
+                return r;
+
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0,
+                               DHCP_OPTION_END, 0, NULL);
+        if (r < 0)
+                return r;
+
+        /* RFC 2131 Section 4.1
+
+           If the ’giaddr’ field in a DHCP message from a client is non-zero,
+           the server sends any return messages to the ’DHCP server’ port on the
+           BOOTP relay agent whose address appears in ’giaddr’. If the ’giaddr’
+           field is zero and the ’ciaddr’ field is nonzero, then the server
+           unicasts DHCPOFFER and DHCPACK messages to the address in ’ciaddr’.
+           If ’giaddr’ is zero and ’ciaddr’ is zero, and the broadcast bit is
+           set, then the server broadcasts DHCPOFFER and DHCPACK messages to
+           0xffffffff. If the broadcast bit is not set and ’giaddr’ is zero and
+           ’ciaddr’ is zero, then the server unicasts DHCPOFFER and DHCPACK
+           messages to the client’s hardware address and ’yiaddr’ address. In
+           all cases, when ’giaddr’ is zero, the server broadcasts any DHCPNAK
+           messages to 0xffffffff.
+
+           Section 4.3.2
+
+           If ’giaddr’ is set in the DHCPREQUEST message, the client is on a
+           different subnet. The server MUST set the broadcast bit in the
+           DHCPNAK, so that the relay agent will broadcast the DHCPNAK to the
+           client, because the client may not have a correct network address
+           or subnet mask, and the client may not be answering ARP requests.
+         */
+        if (req->message->giaddr) {
+                destination = req->message->giaddr;
+                if (type == DHCP_NAK)
+                        packet->dhcp.flags = htobe16(0x8000);
+        } else if (req->message->ciaddr && type != DHCP_NAK)
+                destination = req->message->ciaddr;
+
+        if (destination != INADDR_ANY)
+                return dhcp_server_send_udp(server, destination, &packet->dhcp,
+                                            sizeof(DHCPMessage) + optoffset);
+        else if (requested_broadcast(req) || type == DHCP_NAK)
+                return dhcp_server_send_udp(server, INADDR_BROADCAST,
+                                            &packet->dhcp,
+                                            sizeof(DHCPMessage) + optoffset);
+        else
+                /* we cannot send UDP packet to specific MAC address when the
+                   address is not yet configured, so must fall back to raw
+                   packets */
+                return dhcp_server_send_unicast_raw(server, packet,
+                                                    sizeof(DHCPPacket) + optoffset);
+}
+
+static int server_message_init(sd_dhcp_server *server, DHCPPacket **ret,
+                               uint8_t type, size_t *_optoffset,
+                               DHCPRequest *req) {
+        _cleanup_free_ DHCPPacket *packet = NULL;
+        size_t optoffset = 0;
+        int r;
+
+        assert(server);
+        assert(ret);
+        assert(_optoffset);
+        assert(IN_SET(type, DHCP_OFFER, DHCP_ACK, DHCP_NAK));
+
+        packet = malloc0(sizeof(DHCPPacket) + req->max_optlen);
+        if (!packet)
+                return -ENOMEM;
+
+        r = dhcp_message_init(&packet->dhcp, BOOTREPLY,
+                              be32toh(req->message->xid), type, ARPHRD_ETHER,
+                              req->max_optlen, &optoffset);
+        if (r < 0)
+                return r;
+
+        packet->dhcp.flags = req->message->flags;
+        packet->dhcp.giaddr = req->message->giaddr;
+        memcpy(&packet->dhcp.chaddr, &req->message->chaddr, ETH_ALEN);
+
+        *_optoffset = optoffset;
+        *ret = packet;
+        packet = NULL;
+
+        return 0;
+}
+
+static int server_send_offer(sd_dhcp_server *server, DHCPRequest *req,
+                             be32_t address) {
+        _cleanup_free_ DHCPPacket *packet = NULL;
+        size_t offset;
+        be32_t lease_time;
+        int r;
+
+        r = server_message_init(server, &packet, DHCP_OFFER, &offset, req);
+        if (r < 0)
+                return r;
+
+        packet->dhcp.yiaddr = address;
+
+        lease_time = htobe32(req->lifetime);
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0,
+                               DHCP_OPTION_IP_ADDRESS_LEASE_TIME, 4,
+                               &lease_time);
+        if (r < 0)
+                return r;
+
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0,
+                               DHCP_OPTION_SUBNET_MASK, 4, &server->netmask);
+        if (r < 0)
+                return r;
+
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0,
+                               DHCP_OPTION_ROUTER, 4, &server->address);
+        if (r < 0)
+                return r;
+
+        r = dhcp_server_send_packet(server, req, packet, DHCP_OFFER, offset);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int server_send_ack(sd_dhcp_server *server, DHCPRequest *req,
+                           be32_t address) {
+        _cleanup_free_ DHCPPacket *packet = NULL;
+        size_t offset;
+        be32_t lease_time;
+        int r;
+
+        r = server_message_init(server, &packet, DHCP_ACK, &offset, req);
+        if (r < 0)
+                return r;
+
+        packet->dhcp.yiaddr = address;
+
+        lease_time = htobe32(req->lifetime);
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0,
+                               DHCP_OPTION_IP_ADDRESS_LEASE_TIME, 4,
+                               &lease_time);
+        if (r < 0)
+                return r;
+
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0,
+                               DHCP_OPTION_SUBNET_MASK, 4, &server->netmask);
+        if (r < 0)
+                return r;
+
+        r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0,
+                               DHCP_OPTION_ROUTER, 4, &server->address);
+        if (r < 0)
+                return r;
+
+        r = dhcp_server_send_packet(server, req, packet, DHCP_ACK, offset);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int server_send_nak(sd_dhcp_server *server, DHCPRequest *req) {
+        _cleanup_free_ DHCPPacket *packet = NULL;
+        size_t offset;
+        int r;
+
+        r = server_message_init(server, &packet, DHCP_NAK, &offset, req);
+        if (r < 0)
+                return r;
+
+        r = dhcp_server_send_packet(server, req, packet, DHCP_NAK, offset);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int server_send_forcerenew(sd_dhcp_server *server, be32_t address,
+                                  be32_t gateway, uint8_t chaddr[]) {
+        _cleanup_free_ DHCPPacket *packet = NULL;
+        size_t optoffset = 0;
+        int r;
+
+        assert(server);
+        assert(address != INADDR_ANY);
+        assert(chaddr);
+
+        packet = malloc0(sizeof(DHCPPacket) + DHCP_MIN_OPTIONS_SIZE);
+        if (!packet)
+                return -ENOMEM;
+
+        r = dhcp_message_init(&packet->dhcp, BOOTREPLY, 0,
+                              DHCP_FORCERENEW, ARPHRD_ETHER,
+                              DHCP_MIN_OPTIONS_SIZE, &optoffset);
+        if (r < 0)
+                return r;
+
+        r = dhcp_option_append(&packet->dhcp, DHCP_MIN_OPTIONS_SIZE,
+                               &optoffset, 0, DHCP_OPTION_END, 0, NULL);
+        if (r < 0)
+                return r;
+
+        memcpy(&packet->dhcp.chaddr, chaddr, ETH_ALEN);
+
+        r = dhcp_server_send_udp(server, address, &packet->dhcp,
+                                 sizeof(DHCPMessage) + optoffset);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int parse_request(uint8_t code, uint8_t len, const uint8_t *option,
+                         void *user_data) {
+        DHCPRequest *req = user_data;
+
+        assert(req);
+
+        switch(code) {
+        case DHCP_OPTION_IP_ADDRESS_LEASE_TIME:
+                if (len == 4)
+                        req->lifetime = be32toh(*(be32_t*)option);
+
+                break;
+        case DHCP_OPTION_REQUESTED_IP_ADDRESS:
+                if (len == 4)
+                        req->requested_ip = *(be32_t*)option;
+
+                break;
+        case DHCP_OPTION_SERVER_IDENTIFIER:
+                if (len == 4)
+                        req->server_id = *(be32_t*)option;
+
+                break;
+        case DHCP_OPTION_CLIENT_IDENTIFIER:
+                if (len >= 2) {
+                        uint8_t *data;
+
+                        data = memdup(option, len);
+                        if (!data)
+                                return -ENOMEM;
+
+                        free(req->client_id.data);
+                        req->client_id.data = data;
+                        req->client_id.length = len;
+                }
+
+                break;
+        case DHCP_OPTION_MAXIMUM_MESSAGE_SIZE:
+                if (len == 2)
+                        req->max_optlen = be16toh(*(be16_t*)option) -
+                                          - sizeof(DHCPPacket);
+
+                break;
+        }
+
+        return 0;
+}
+
+static void dhcp_request_free(DHCPRequest *req) {
+        if (!req)
+                return;
+
+        free(req->client_id.data);
+        free(req);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DHCPRequest*, dhcp_request_free);
+#define _cleanup_dhcp_request_free_ _cleanup_(dhcp_request_freep)
+
+static int ensure_sane_request(DHCPRequest *req, DHCPMessage *message) {
+        assert(req);
+        assert(message);
+
+        req->message = message;
+
+        /* set client id based on MAC address if client did not send an explicit
+           one */
+        if (!req->client_id.data) {
+                uint8_t *data;
+
+                data = new0(uint8_t, ETH_ALEN + 1);
+                if (!data)
+                        return -ENOMEM;
+
+                req->client_id.length = ETH_ALEN + 1;
+                req->client_id.data = data;
+                req->client_id.data[0] = 0x01;
+                memcpy(&req->client_id.data[1], &message->chaddr, ETH_ALEN);
+        }
+
+        if (req->max_optlen < DHCP_MIN_OPTIONS_SIZE)
+                req->max_optlen = DHCP_MIN_OPTIONS_SIZE;
+
+        if (!req->lifetime)
+                req->lifetime = DHCP_DEFAULT_LEASE_TIME;
+
+        return 0;
+}
+
+static int get_pool_offset(sd_dhcp_server *server, be32_t requested_ip) {
+        assert(server);
+
+        if (!server->pool_size)
+                return -EINVAL;
+
+        if (be32toh(requested_ip) < be32toh(server->pool_start) ||
+            be32toh(requested_ip) >= be32toh(server->pool_start) +
+                                             + server->pool_size)
+                return -EINVAL;
+
+        return be32toh(requested_ip) - be32toh(server->pool_start);
+}
+
+int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message,
+                               size_t length) {
+        _cleanup_dhcp_request_free_ DHCPRequest *req = NULL;
+        DHCPLease *existing_lease;
+        int type, r;
+
+        assert(server);
+        assert(message);
+
+        if (message->op != BOOTREQUEST ||
+            message->htype != ARPHRD_ETHER ||
+            message->hlen != ETHER_ADDR_LEN)
+                return 0;
+
+        req = new0(DHCPRequest, 1);
+        if (!req)
+                return -ENOMEM;
+
+        type = dhcp_option_parse(message, length, parse_request, req);
+        if (type < 0)
+                return 0;
+
+        r = ensure_sane_request(req, message);
+        if (r < 0)
+                /* this only fails on critical errors */
+                return r;
+
+        existing_lease = hashmap_get(server->leases_by_client_id,
+                                     &req->client_id);
+
+        switch(type) {
+        case DHCP_DISCOVER:
+        {
+                be32_t address = INADDR_ANY;
+                unsigned i;
+
+                log_dhcp_server(server, "DISCOVER (0x%x)",
+                                be32toh(req->message->xid));
+
+                if (!server->pool_size)
+                        /* no pool allocated */
+                        return 0;
+
+                /* for now pick a random free address from the pool */
+                if (existing_lease)
+                        address = existing_lease->address;
+                else {
+                        for (i = 0; i < server->pool_size; i++) {
+                                if (!server->bound_leases[server->next_offer]) {
+                                        address = htobe32(be32toh(server->pool_start) + server->next_offer);
+                                        break;
+                                } else
+                                        server->next_offer = (server->next_offer + 1) % server->pool_size;
+                        }
+                }
+
+                if (address == INADDR_ANY)
+                        /* no free addresses left */
+                        return 0;
+
+                r = server_send_offer(server, req, address);
+                if (r < 0) {
+                        /* this only fails on critical errors */
+                        log_dhcp_server(server, "could not send offer: %s",
+                                        strerror(-r));
+                        return r;
+                } else {
+                        log_dhcp_server(server, "OFFER (0x%x)",
+                                        be32toh(req->message->xid));
+                        return DHCP_OFFER;
+                }
+
+                break;
+        }
+        case DHCP_DECLINE:
+                log_dhcp_server(server, "DECLINE (0x%x)",
+                                be32toh(req->message->xid));
+
+                /* TODO: make sure we don't offer this address again */
+
+                return 1;
+
+                break;
+        case DHCP_REQUEST:
+        {
+                be32_t address;
+                bool init_reboot = false;
+                int pool_offset;
+
+                /* see RFC 2131, section 4.3.2 */
+
+                if (req->server_id) {
+                        log_dhcp_server(server, "REQUEST (selecting) (0x%x)",
+                                        be32toh(req->message->xid));
+
+                        /* SELECTING */
+                        if (req->server_id != server->address)
+                                /* client did not pick us */
+                                return 0;
+
+                        if (req->message->ciaddr)
+                                /* this MUST be zero */
+                                return 0;
+
+                        if (!req->requested_ip)
+                                /* this must be filled in with the yiaddr
+                                   from the chosen OFFER */
+                                return 0;
+
+                        address = req->requested_ip;
+                } else if (req->requested_ip) {
+                        log_dhcp_server(server, "REQUEST (init-reboot) (0x%x)",
+                                        be32toh(req->message->xid));
+
+                        /* INIT-REBOOT */
+                        if (req->message->ciaddr)
+                                /* this MUST be zero */
+                                return 0;
+
+                        /* TODO: check more carefully if IP is correct */
+                        address = req->requested_ip;
+                        init_reboot = true;
+                } else {
+                        log_dhcp_server(server, "REQUEST (rebinding/renewing) (0x%x)",
+                                        be32toh(req->message->xid));
+
+                        /* REBINDING / RENEWING */
+                        if (!req->message->ciaddr)
+                                /* this MUST be filled in with clients IP address */
+                                return 0;
+
+                        address = req->message->ciaddr;
+                }
+
+                pool_offset = get_pool_offset(server, address);
+
+                /* verify that the requested address is from the pool, and either
+                   owned by the current client or free */
+                if (pool_offset >= 0 &&
+                    server->bound_leases[pool_offset] == existing_lease) {
+                        DHCPLease *lease;
+                        usec_t time_now;
+
+                        if (!existing_lease) {
+                                lease = new0(DHCPLease, 1);
+                                lease->address = req->requested_ip;
+                                lease->client_id.data = memdup(req->client_id.data,
+                                                               req->client_id.length);
+                                if (!lease->client_id.data) {
+                                        free(lease);
+                                        return -ENOMEM;
+                                }
+                                lease->client_id.length = req->client_id.length;
+                                memcpy(&lease->chaddr, &req->message->chaddr,
+                                       ETH_ALEN);
+                                lease->gateway = req->message->giaddr;
+                        } else
+                                lease = existing_lease;
+
+                        r = sd_event_now(server->event,
+                                         clock_boottime_or_monotonic(),
+                                         &time_now);
+                        if (r < 0)
+                                time_now = now(clock_boottime_or_monotonic());
+                        lease->expiration = req->lifetime * USEC_PER_SEC + time_now;
+
+                        r = server_send_ack(server, req, address);
+                        if (r < 0) {
+                                /* this only fails on critical errors */
+                                log_dhcp_server(server, "could not send ack: %s",
+                                                strerror(-r));
+
+                                if (!existing_lease)
+                                        dhcp_lease_free(lease);
+
+                                return r;
+                        } else {
+                                log_dhcp_server(server, "ACK (0x%x)",
+                                                be32toh(req->message->xid));
+
+                                server->bound_leases[pool_offset] = lease;
+                                hashmap_put(server->leases_by_client_id,
+                                            &lease->client_id, lease);
+
+                                return DHCP_ACK;
+                        }
+                } else if (init_reboot) {
+                        r = server_send_nak(server, req);
+                        if (r < 0) {
+                                /* this only fails on critical errors */
+                                log_dhcp_server(server, "could not send nak: %s",
+                                                strerror(-r));
+                                return r;
+                        } else {
+                                log_dhcp_server(server, "NAK (0x%x)",
+                                                be32toh(req->message->xid));
+                                return DHCP_NAK;
+                        }
+                }
+
+                break;
+        }
+        case DHCP_RELEASE: {
+                int pool_offset;
+
+                log_dhcp_server(server, "RELEASE (0x%x)",
+                                be32toh(req->message->xid));
+
+                if (!existing_lease)
+                        return 0;
+
+                if (existing_lease->address != req->message->ciaddr)
+                        return 0;
+
+                pool_offset = get_pool_offset(server, req->message->ciaddr);
+                if (pool_offset < 0)
+                        return 0;
+
+                if (server->bound_leases[pool_offset] == existing_lease) {
+                        server->bound_leases[pool_offset] = NULL;
+                        hashmap_remove(server->leases_by_client_id, existing_lease);
+                        dhcp_lease_free(existing_lease);
+
+                        return 1;
+                } else
+                        return 0;
+        }
+        }
+
+        return 0;
+}
+
+static int server_receive_message(sd_event_source *s, int fd,
+                                  uint32_t revents, void *userdata) {
+        _cleanup_free_ DHCPMessage *message = NULL;
+        uint8_t cmsgbuf[CMSG_LEN(sizeof(struct in_pktinfo))];
+        sd_dhcp_server *server = userdata;
+        struct iovec iov = {};
+        struct msghdr msg = {
+                .msg_iov = &iov,
+                .msg_iovlen = 1,
+                .msg_control = cmsgbuf,
+                .msg_controllen = sizeof(cmsgbuf),
+        };
+        struct cmsghdr *cmsg;
+        int buflen = 0, len, r;
+
+        assert(server);
+
+        r = ioctl(fd, FIONREAD, &buflen);
+        if (r < 0)
+                return r;
+        if (buflen < 0)
+                return -EIO;
+
+        message = malloc0(buflen);
+        if (!message)
+                return -ENOMEM;
+
+        iov.iov_base = message;
+        iov.iov_len = buflen;
+
+        len = recvmsg(fd, &msg, 0);
+        if (len < buflen)
+                return 0;
+        else if ((size_t)len < sizeof(DHCPMessage))
+                return 0;
+
+        for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+                if (cmsg->cmsg_level == IPPROTO_IP &&
+                    cmsg->cmsg_type == IP_PKTINFO &&
+                    cmsg->cmsg_len == CMSG_LEN(sizeof(struct in_pktinfo))) {
+                        struct in_pktinfo *info = (struct in_pktinfo*)CMSG_DATA(cmsg);
+
+                        /* TODO figure out if this can be done as a filter on
+                         * the socket, like for IPv6 */
+                        if (server->index != info->ipi_ifindex)
+                                return 0;
+
+                        break;
+                }
+        }
+
+        return dhcp_server_handle_message(server, message, (size_t)len);
+}
+
+int sd_dhcp_server_start(sd_dhcp_server *server) {
+        int r;
+
+        assert_return(server, -EINVAL);
+        assert_return(server->event, -EINVAL);
+        assert_return(!server->receive_message, -EBUSY);
+        assert_return(server->fd_raw == -1, -EBUSY);
+        assert_return(server->fd == -1, -EBUSY);
+        assert_return(server->address != htobe32(INADDR_ANY), -EUNATCH);
+
+        r = socket(AF_PACKET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
+        if (r < 0) {
+                r = -errno;
+                sd_dhcp_server_stop(server);
+                return r;
+        }
+        server->fd_raw = r;
+
+        r = dhcp_network_bind_udp_socket(INADDR_ANY, DHCP_PORT_SERVER);
+        if (r < 0) {
+                sd_dhcp_server_stop(server);
+                return r;
+        }
+        server->fd = r;
+
+        r = sd_event_add_io(server->event, &server->receive_message,
+                            server->fd, EPOLLIN,
+                            server_receive_message, server);
+        if (r < 0) {
+                sd_dhcp_server_stop(server);
+                return r;
+        }
+
+        r = sd_event_source_set_priority(server->receive_message,
+                                         server->event_priority);
+        if (r < 0) {
+                sd_dhcp_server_stop(server);
+                return r;
+        }
+
+        log_dhcp_server(server, "STARTED");
+
+        return 0;
+}
+
+int sd_dhcp_server_forcerenew(sd_dhcp_server *server) {
+        unsigned i;
+        int r = 0;
+
+        assert_return(server, -EINVAL);
+        assert(server->bound_leases);
+
+        for (i = 0; i < server->pool_size; i++) {
+                DHCPLease *lease = server->bound_leases[i];
+
+                if (!lease)
+                        continue;
+
+                r = server_send_forcerenew(server, lease->address,
+                                           lease->gateway,
+                                           lease->chaddr);
+                if (r < 0)
+                        return r;
+                else
+                        log_dhcp_server(server, "FORCERENEW");
+        }
+
+        return r;
+}