dhcpv4: offer a valid configuration with DHCP NAK
[project/odhcpd.git] / src / dhcpv6-ia.c
index 593b5bd..90dcaf5 100644 (file)
@@ -22,6 +22,7 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <stdio.h>
+#include <poll.h>
 #include <alloca.h>
 #include <resolv.h>
 #include <limits.h>
@@ -50,10 +51,13 @@ int dhcpv6_ia_init(void)
 void free_dhcpv6_assignment(struct dhcpv6_assignment *c)
 {
        if (c->managed_sock.fd.registered) {
-               close(c->managed_sock.fd.fd);
                ustream_free(&c->managed_sock.stream);
+               close(c->managed_sock.fd.fd);
        }
 
+       if (c->head.next)
+               list_del(&c->head);
+
        free(c->managed);
        free(c->hostname);
        free(c->classes);
@@ -67,7 +71,6 @@ int setup_dhcpv6_ia_interface(struct interface *iface, bool enable)
                struct dhcpv6_assignment *c;
                while (!list_empty(&iface->ia_assignments)) {
                        c = list_first_entry(&iface->ia_assignments, struct dhcpv6_assignment, head);
-                       list_del(&c->head);
                        free_dhcpv6_assignment(c);
                }
        }
@@ -255,6 +258,8 @@ void dhcpv6_write_statefile(void)
                                        for (size_t i = 0; i < addrlen; ++i) {
                                                if (addrs[i].prefix > 96)
                                                        continue;
+                                               if (c->valid_until <= now)
+                                                       continue;
 
                                                addr = addrs[i].addr;
                                                if (c->length == 128)
@@ -264,7 +269,7 @@ void dhcpv6_write_statefile(void)
 
                                                inet_ntop(AF_INET6, &addr, ipbuf, sizeof(ipbuf) - 1);
 
-                                               if (c->length == 128 && c->hostname && i == 0) {
+                                               if (c->length == 128 && c->hostname) {
                                                        fputs(ipbuf, fp);
 
                                                        char b[256];
@@ -277,7 +282,7 @@ void dhcpv6_write_statefile(void)
                                                        md5_hash(c->hostname, strlen(c->hostname), &md5);
                                                }
 
-                                               l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/%hhu ", ipbuf,
+                                               l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/%d ", ipbuf,
                                                                (c->managed_size) ? addrs[i].prefix : c->length);
                                        }
                                        leasebuf[l - 1] = '\n';
@@ -401,8 +406,22 @@ static void managed_handle_pd_data(struct ustream *s, _unused int bytes_new)
                        if (sscanf(x, "%u", &n->valid) < 1)
                                continue;
 
-                       n->preferred += now;
-                       n->valid += now;
+                       if (n->preferred > n->valid)
+                               continue;
+
+                       if (UINT32_MAX - now < n->preferred)
+                               n->preferred = UINT32_MAX;
+                       else
+                               n->preferred += now;
+
+                       if (UINT32_MAX - now < n->valid)
+                               n->valid = UINT32_MAX;
+                       else
+                               n->valid += now;
+
+                       n->has_class = false;
+                       n->class = 0;
+                       n->dprefix = 0;
 
                        ++c->managed_size;
                }
@@ -410,10 +429,10 @@ static void managed_handle_pd_data(struct ustream *s, _unused int bytes_new)
                ustream_consume(s, end - data);
        }
 
-       if (first && c->managed_size == 0) {
-               list_del(&c->head);
+       if (first && c->managed_size == 0)
                free_dhcpv6_assignment(c);
-       }
+       else if (first)
+               c->valid_until = now + 150;
 }
 
 
@@ -436,13 +455,26 @@ static bool assign_pd(struct interface *iface, struct dhcpv6_assignment *assign)
        if (iface->dhcpv6_pd_manager[0]) {
                int fd = usock(USOCK_UNIX | USOCK_TCP, iface->dhcpv6_pd_manager, NULL);
                if (fd >= 0) {
+                       char iaidbuf[298];
+                       odhcpd_hexlify(iaidbuf, assign->clid_data, assign->clid_len);
+
                        assign->managed_sock.stream.notify_read = managed_handle_pd_data;
                        assign->managed_sock.stream.notify_state = managed_handle_pd_done;
                        ustream_fd_init(&assign->managed_sock, fd);
-                       ustream_printf(&assign->managed_sock.stream, "::/%d,0,0\n\n", assign->length);
+                       ustream_printf(&assign->managed_sock.stream, "%s,%x\n::/%d,0,0\n\n",
+                                       iaidbuf, assign->iaid, assign->length);
                        ustream_write_pending(&assign->managed_sock.stream);
                        assign->managed_size = -1;
+                       assign->valid_until = odhcpd_time() + 15;
                        list_add(&assign->head, &iface->ia_assignments);
+
+                       // Wait initial period of up to 250ms for immediate assignment
+                       struct pollfd pfd = { .fd = fd, .events = POLLIN };
+                       poll(&pfd, 1, 250);
+                       managed_handle_pd_data(&assign->managed_sock.stream, 0);
+
+                       if (fcntl(fd, F_GETFL) >= 0 && assign->managed_size > 0)
+                               return true;
                }
 
                return false;
@@ -590,7 +622,7 @@ static void update(struct interface *iface)
        if (change) {
                struct dhcpv6_assignment *c;
                list_for_each_entry(c, &iface->ia_assignments, head)
-                       if (c != border)
+                       if (c != border && !iface->managed)
                                apply_lease(iface, c, false);
        }
 
@@ -688,11 +720,18 @@ static size_t append_reply(uint8_t *buf, size_t buflen, uint16_t status,
                datalen += sizeof(stat);
        } else {
                if (a) {
-                       uint32_t pref = 3600;
-                       uint32_t valid = 3600;
+                       uint32_t leasetime = iface->dhcpv4_leasetime;
+                       if (leasetime == 0)
+                               leasetime = 3600;
+                       else if (leasetime < 60)
+                               leasetime = 60;
+
+                       uint32_t pref = leasetime;
+                       uint32_t valid = leasetime;
 
                        struct odhcpd_ipaddr *addrs = (a->managed) ? a->managed : iface->ia_addr;
                        size_t addrlen = (a->managed) ? (size_t)a->managed_size : iface->ia_addr_len;
+
                        for (size_t i = 0; i < addrlen; ++i) {
                                bool match = true;
                                if (addrs[i].has_class) {
@@ -737,7 +776,7 @@ static size_t append_reply(uint8_t *buf, size_t buflen, uint16_t status,
                                                .len = htons(sizeof(p) - 4),
                                                .preferred = htonl(prefix_pref),
                                                .valid = htonl(prefix_valid),
-                                               .prefix = (a->managed_size) ? a->managed_size : a->length,
+                                               .prefix = (a->managed_size) ? addrs[i].prefix : a->length,
                                                .addr = addrs[i].addr
                                        };
                                        p.addr.s6_addr32[1] |= htonl(a->assigned);
@@ -751,7 +790,8 @@ static size_t append_reply(uint8_t *buf, size_t buflen, uint16_t status,
                                        }
 #endif
 
-                                       if (datalen + entrlen + 4 > buflen || a->assigned == 0)
+                                       if (datalen + entrlen + 4 > buflen ||
+                                                       (a->assigned == 0 && a->managed_size == 0))
                                                continue;
 
                                        memcpy(buf + datalen, &p, sizeof(p));
@@ -886,7 +926,7 @@ static size_t append_reply(uint8_t *buf, size_t buflen, uint16_t status,
 }
 
 
-size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
+ssize_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
                const struct sockaddr_in6 *addr, const void *data, const uint8_t *end)
 {
        time_t now = odhcpd_time();
@@ -901,6 +941,8 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
        char hostname[256];
        size_t hostname_len = 0;
        bool class_oro = false;
+       bool notonlink = false;
+
        dhcpv6_for_each_option(start, end, otype, olen, odata) {
                if (otype == DHCPV6_OPT_CLIENTID) {
                        clid_data = odata;
@@ -934,13 +976,12 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
                goto out;
 
        update(iface);
-       bool update_state = false;
-       bool managed_pd_out = false;
 
        struct dhcpv6_assignment *first = NULL;
        dhcpv6_for_each_option(start, end, otype, olen, odata) {
                bool is_pd = (otype == DHCPV6_OPT_IA_PD);
                bool is_na = (otype == DHCPV6_OPT_IA_NA);
+               bool ia_addr_present = false;
                if (!is_pd && !is_na)
                        continue;
 
@@ -993,6 +1034,7 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
                                if (stype != DHCPV6_OPT_IA_ADDR || slen < sizeof(struct dhcpv6_ia_addr) - 4)
                                        continue;
 
+                               ia_addr_present = true;
 #ifdef DHCPV6_OPT_PREFIX_CLASS
                                uint8_t *xdata;
                                uint16_t xtype, xlen;
@@ -1040,9 +1082,7 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
                // Generic message handling
                uint16_t status = DHCPV6_STATUS_OK;
                if (a && a->managed_size < 0) {
-                       managed_pd_out = true;
-                       status = DHCPV6_STATUS_NOTONLINK;
-                       ia_response_len = append_reply(buf, buflen, status, ia, NULL, iface, true);
+                       return -1;
                } else if (hdr->msg_type == DHCPV6_MSG_SOLICIT || hdr->msg_type == DHCPV6_MSG_REQUEST) {
                        bool assigned = !!a;
 
@@ -1070,12 +1110,12 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
 
                                        if (is_pd)
                                                while (!(assigned = assign_pd(iface, a)) &&
-                                                               ++a->length <= 64 && !a->managed_size);
+                                                               !a->managed_size && ++a->length <= 64);
                                        else
                                                assigned = assign_na(iface, a);
 
                                        if (a->managed_size && !assigned)
-                                               managed_pd_out = true;
+                                               return -1;
                                }
                        }
 
@@ -1113,7 +1153,7 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
 
                        // Was only a solicitation: mark binding for removal
                        if (assigned && hdr->msg_type == DHCPV6_MSG_SOLICIT) {
-                               a->valid_until = now + 15;
+                               a->valid_until = 0;
                        } else if (assigned && hdr->msg_type == DHCPV6_MSG_REQUEST) {
                                if (hostname_len > 0) {
                                        a->hostname = realloc(a->hostname, hostname_len + 1);
@@ -1124,8 +1164,7 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
                                }
                                a->accept_reconf = accept_reconf;
                                apply_lease(iface, a, true);
-                               update_state = true;
-                       } else if (!assigned && a) { // Cleanup failed assignment
+                       } else if (!assigned && a && a->managed_size == 0) { // Cleanup failed assignment
                                free_dhcpv6_assignment(a);
                        }
                } else if (hdr->msg_type == DHCPV6_MSG_RENEW ||
@@ -1143,16 +1182,15 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
                        } else if (hdr->msg_type == DHCPV6_MSG_RELEASE) {
                                a->valid_until = 0;
                                apply_lease(iface, a, false);
-                               update_state = true;
                        } else if (hdr->msg_type == DHCPV6_MSG_DECLINE && a->length == 128) {
                                a->clid_len = 0;
                                a->valid_until = now + 3600; // Block address for 1h
-                               update_state = true;
                        }
-               } else if (hdr->msg_type == DHCPV6_MSG_CONFIRM) {
-                       // Always send NOTONLINK for CONFIRM so that clients restart connection
+               } else if (hdr->msg_type == DHCPV6_MSG_CONFIRM && ia_addr_present) {
+                       // Send NOTONLINK for CONFIRM with addr present so that clients restart connection
                        status = DHCPV6_STATUS_NOTONLINK;
                        ia_response_len = append_reply(buf, buflen, status, ia, a, iface, true);
+                       notonlink = true;
                }
 
                buf += ia_response_len;
@@ -1160,26 +1198,18 @@ size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
                response_len += ia_response_len;
        }
 
-       if (hdr->msg_type == DHCPV6_MSG_RELEASE && response_len + 6 < buflen) {
+       if ((hdr->msg_type == DHCPV6_MSG_RELEASE || hdr->msg_type == DHCPV6_MSG_DECLINE || notonlink) &&
+                       response_len + 6 < buflen) {
                buf[0] = 0;
                buf[1] = DHCPV6_OPT_STATUS;
                buf[2] = 0;
                buf[3] = 2;
                buf[4] = 0;
-               buf[5] = DHCPV6_STATUS_OK;
-               response_len += 6;
-       } else if (managed_pd_out && response_len + 6 < buflen) {
-               buf[0] = 0;
-               buf[1] = DHCPV6_OPT_STATUS;
-               buf[2] = 0;
-               buf[3] = 2;
-               buf[4] = 0;
-               buf[5] = DHCPV6_STATUS_NOADDRSAVAIL;
+               buf[5] = (notonlink) ? DHCPV6_STATUS_NOTONLINK : DHCPV6_STATUS_OK;
                response_len += 6;
        }
 
-       if (update_state)
-               dhcpv6_write_statefile();
+       dhcpv6_write_statefile();
 
 out:
        return response_len;