chiark / gitweb /
sd-dhcp-server: check if r < 0
[elogind.git] / src / libsystemd-network / sd-dhcp-server.c
index 75f43165503402a03615e46e8b43e562cb86022d..2d3eaa35e396c245fe5b1e0369f73decc44dc178 100644 (file)
 #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         60
+#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) {
@@ -37,6 +39,11 @@ int sd_dhcp_server_set_lease_pool(sd_dhcp_server *server, struct in_addr *addres
         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;
@@ -55,6 +62,12 @@ int sd_dhcp_server_set_address(sd_dhcp_server *server, struct in_addr *address)
         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)
                 assert_se(REFCNT_INC(server->n_ref) >= 2);
@@ -62,15 +75,63 @@ 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 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_dhcp_server_stop(server);
+        if (!server)
+                return NULL;
 
-                sd_event_unref(server->event);
-                free(server);
-        }
+        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;
 }
@@ -90,6 +151,7 @@ int sd_dhcp_server_new(sd_dhcp_server **ret, int ifindex) {
         server->fd = -1;
         server->address = htobe32(INADDR_ANY);
         server->index = ifindex;
+        server->leases_by_client_id = hashmap_new(client_id_hash_func, client_id_compare_func);
 
         *ret = server;
         server = NULL;
@@ -284,9 +346,12 @@ int dhcp_server_send_packet(sd_dhcp_server *server,
         } else if (req->message->ciaddr && type != DHCP_NAK)
                 destination = req->message->ciaddr;
 
-        if (destination || requested_broadcast(req) || type == DHCP_NAK)
+        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 */
@@ -297,7 +362,7 @@ int dhcp_server_send_packet(sd_dhcp_server *server,
 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;
+        size_t optoffset = 0;
         int r;
 
         assert(server);
@@ -337,8 +402,7 @@ static int server_send_offer(sd_dhcp_server *server, DHCPRequest *req, be32_t ad
 
         packet->dhcp.yiaddr = address;
 
-        /* for one minute */
-        lease_time = htobe32(DHCP_DEFAULT_LEASE_TIME);
+        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)
@@ -363,8 +427,7 @@ static int server_send_ack(sd_dhcp_server *server, DHCPRequest *req, be32_t addr
 
         packet->dhcp.yiaddr = address;
 
-        /* for ten seconds */
-        lease_time = htobe32(DHCP_DEFAULT_LEASE_TIME);
+        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)
@@ -393,6 +456,41 @@ static int server_send_nak(sd_dhcp_server *server, DHCPRequest *req) {
         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, 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;
@@ -400,6 +498,11 @@ static int parse_request(uint8_t code, uint8_t len, const uint8_t *option,
         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;
@@ -469,12 +572,30 @@ static int ensure_sane_request(DHCPRequest *req, DHCPMessage *message) {
         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);
@@ -498,10 +619,13 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message,
                 /* 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;
+                be32_t address = INADDR_ANY;
+                unsigned i;
 
                 log_dhcp_server(server, "DISCOVER (0x%x)",
                                 be32toh(req->message->xid));
@@ -510,9 +634,22 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message,
                         /* no pool allocated */
                         return 0;
 
-                /* for now pick a random address from the pool */
-                address = htobe32(be32toh(server->pool_start) +
-                                      (random_u32() % server->pool_size));
+                /* 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) {
@@ -528,10 +665,20 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message,
 
                 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 */
 
@@ -578,20 +725,52 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message,
                         address = req->message->ciaddr;
                 }
 
-                /* for now we just verify that the address is from the pool, not
-                   whether or not it is taken */
-                if (htobe32(req->requested_ip) >= htobe32(server->pool_start) &&
-                    htobe32(req->requested_ip) < htobe32(server->pool_start) +
-                                                  + server->pool_size) {
+                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) {
@@ -610,6 +789,31 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message,
 
                 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;
@@ -712,3 +916,28 @@ int sd_dhcp_server_start(sd_dhcp_server *server) {
 
         return 0;
 }
+
+int sd_dhcp_server_forcerenew(sd_dhcp_server *server) {
+        unsigned i;
+        int r;
+
+        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;
+}