/** * Copyright (C) 2013 Steven Barth * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License v2 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #include "odhcpd.h" #include "dhcpv6.h" #include "dhcpv4.h" #include "libubox/md5.h" #include "libubox/usock.h" #include #include #include #include #include #include #include #include #include #include #include #include #include static void update(struct interface *iface); static void reconf_timer(struct uloop_timeout *event); static struct uloop_timeout reconf_event = {.cb = reconf_timer}; static uint32_t serial = 0; static uint8_t statemd5[16]; int dhcpv6_ia_init(void) { uloop_timeout_set(&reconf_event, 2000); return 0; } void free_dhcpv6_assignment(struct dhcpv6_assignment *c) { if (c->managed_sock.fd.registered) { 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); free(c); } int setup_dhcpv6_ia_interface(struct interface *iface, bool enable) { if (!enable && iface->ia_assignments.next) { struct dhcpv6_assignment *c; while (!list_empty(&iface->ia_assignments)) { c = list_first_entry(&iface->ia_assignments, struct dhcpv6_assignment, head); free_dhcpv6_assignment(c); } } if (iface->dhcpv6 == RELAYD_SERVER) { if (!iface->ia_assignments.next) INIT_LIST_HEAD(&iface->ia_assignments); if (list_empty(&iface->ia_assignments)) { struct dhcpv6_assignment *border = calloc(1, sizeof(*border)); if (!border) { syslog(LOG_ERR, "Calloc failed for border on interface %s", iface->ifname); return -1; } border->length = 64; list_add(&border->head, &iface->ia_assignments); } update(iface); // Parse static entries struct lease *lease; list_for_each_entry(lease, &leases, head) { // Construct entry struct dhcpv6_assignment *a = calloc(1, sizeof(*a) + lease->duid_len); if (!a) { syslog(LOG_ERR, "Calloc failed for static lease assignment on interface %s", iface->ifname); return -1; } a->clid_len = lease->duid_len; a->length = 128; if (lease->hostid) { a->assigned = lease->hostid; } else { uint32_t i4a = ntohl(lease->ipaddr.s_addr) & 0xff; a->assigned = ((i4a / 100) << 8) | (((i4a % 100) / 10) << 4) | (i4a % 10); } odhcpd_urandom(a->key, sizeof(a->key)); memcpy(a->clid_data, lease->duid, a->clid_len); memcpy(a->mac, lease->mac.ether_addr_octet, sizeof(a->mac)); // Assign to all interfaces struct dhcpv6_assignment *c; list_for_each_entry(c, &iface->ia_assignments, head) { if (c->length != 128 || c->assigned > a->assigned) { list_add_tail(&a->head, &c->head); break; } else if (c->assigned == a->assigned) { // Already an assignment with that number break; } } if (a->head.next) { if (lease->hostname[0]) { free(a->hostname); a->hostname = strdup(lease->hostname); } } else { free(a->classes); free(a->hostname); free(a); } } } return 0; } static int send_reconf(struct interface *iface, struct dhcpv6_assignment *assign) { struct { struct dhcpv6_client_header hdr; uint16_t srvid_type; uint16_t srvid_len; uint16_t duid_type; uint16_t hardware_type; uint8_t mac[6]; uint16_t msg_type; uint16_t msg_len; uint8_t msg_id; struct dhcpv6_auth_reconfigure auth; uint16_t clid_type; uint16_t clid_len; uint8_t clid_data[128]; } __attribute__((packed)) reconf_msg = { .hdr = {DHCPV6_MSG_RECONFIGURE, {0, 0, 0}}, .srvid_type = htons(DHCPV6_OPT_SERVERID), .srvid_len = htons(10), .duid_type = htons(3), .hardware_type = htons(1), .msg_type = htons(DHCPV6_OPT_RECONF_MSG), .msg_len = htons(1), .msg_id = DHCPV6_MSG_RENEW, .auth = {htons(DHCPV6_OPT_AUTH), htons(sizeof(reconf_msg.auth) - 4), 3, 1, 0, {htonl(time(NULL)), htonl(++serial)}, 2, {0}}, .clid_type = htons(DHCPV6_OPT_CLIENTID), .clid_len = htons(assign->clid_len), .clid_data = {0}, }; odhcpd_get_mac(iface, reconf_msg.mac); memcpy(reconf_msg.clid_data, assign->clid_data, assign->clid_len); struct iovec iov = {&reconf_msg, sizeof(reconf_msg) - 128 + assign->clid_len}; md5_ctx_t md5; uint8_t secretbytes[64]; memset(secretbytes, 0, sizeof(secretbytes)); memcpy(secretbytes, assign->key, sizeof(assign->key)); for (size_t i = 0; i < sizeof(secretbytes); ++i) secretbytes[i] ^= 0x36; md5_begin(&md5); md5_hash(secretbytes, sizeof(secretbytes), &md5); md5_hash(iov.iov_base, iov.iov_len, &md5); md5_end(reconf_msg.auth.key, &md5); for (size_t i = 0; i < sizeof(secretbytes); ++i) { secretbytes[i] ^= 0x36; secretbytes[i] ^= 0x5c; } md5_begin(&md5); md5_hash(secretbytes, sizeof(secretbytes), &md5); md5_hash(reconf_msg.auth.key, 16, &md5); md5_end(reconf_msg.auth.key, &md5); return odhcpd_send(iface->dhcpv6_event.uloop.fd, &assign->peer, &iov, 1, iface); } void dhcpv6_write_statefile(void) { md5_ctx_t md5; md5_begin(&md5); if (config.dhcp_statefile) { time_t now = odhcpd_time(), wall_time = time(NULL); int fd = open(config.dhcp_statefile, O_CREAT | O_WRONLY | O_CLOEXEC, 0644); if (fd < 0) return; lockf(fd, F_LOCK, 0); ftruncate(fd, 0); FILE *fp = fdopen(fd, "w"); if (!fp) { close(fd); return; } struct interface *iface; list_for_each_entry(iface, &interfaces, head) { if (iface->dhcpv6 != RELAYD_SERVER && iface->dhcpv4 != RELAYD_SERVER) continue; if (iface->dhcpv6 == RELAYD_SERVER && iface->ia_assignments.next) { struct dhcpv6_assignment *c; list_for_each_entry(c, &iface->ia_assignments, head) { if (c->clid_len == 0 || c->managed_size < 0) continue; char ipbuf[INET6_ADDRSTRLEN]; char leasebuf[512]; char duidbuf[264]; odhcpd_hexlify(duidbuf, c->clid_data, c->clid_len); // iface DUID iaid hostname lifetime assigned length [addrs...] int l = snprintf(leasebuf, sizeof(leasebuf), "# %s %s %x %s %u %x %u ", iface->ifname, duidbuf, ntohl(c->iaid), (c->hostname ? c->hostname : "-"), (unsigned)(c->valid_until > now ? (c->valid_until - now + wall_time) : 0), c->assigned, (unsigned)c->length); struct in6_addr addr; struct odhcpd_ipaddr *addrs = (c->managed) ? c->managed : iface->ia_addr; size_t addrlen = (c->managed) ? (size_t)c->managed_size : iface->ia_addr_len; for (size_t i = 0; i < addrlen; ++i) { if (addrs[i].prefix > 96) continue; addr = addrs[i].addr; if (c->length == 128) addr.s6_addr32[3] = htonl(c->assigned); else addr.s6_addr32[1] |= htonl(c->assigned); inet_ntop(AF_INET6, &addr, ipbuf, sizeof(ipbuf) - 1); if (c->length == 128 && c->hostname && i == 0) { fputs(ipbuf, fp); char b[256]; if (dn_expand(iface->search, iface->search + iface->search_len, iface->search, b, sizeof(b)) > 0) fprintf(fp, "\t%s.%s", c->hostname, b); fprintf(fp, "\t%s\n", c->hostname); md5_hash(ipbuf, strlen(ipbuf), &md5); md5_hash(c->hostname, strlen(c->hostname), &md5); } l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/%hhu ", ipbuf, (c->managed_size) ? addrs[i].prefix : c->length); } leasebuf[l - 1] = '\n'; fwrite(leasebuf, 1, l, fp); } } if (iface->dhcpv4 == RELAYD_SERVER && iface->dhcpv4_assignments.next) { struct dhcpv4_assignment *c; list_for_each_entry(c, &iface->dhcpv4_assignments, head) { char ipbuf[INET6_ADDRSTRLEN]; char leasebuf[512]; char duidbuf[16]; odhcpd_hexlify(duidbuf, c->hwaddr, sizeof(c->hwaddr)); // iface DUID iaid hostname lifetime assigned length [addrs...] int l = snprintf(leasebuf, sizeof(leasebuf), "# %s %s ipv4 %s %u %x 32 ", iface->ifname, duidbuf, (c->hostname ? c->hostname : "-"), (unsigned)(c->valid_until > now ? (c->valid_until - now + wall_time) : 0), c->addr); struct in_addr addr = {htonl(c->addr)}; inet_ntop(AF_INET, &addr, ipbuf, sizeof(ipbuf) - 1); if (c->hostname[0]) { fputs(ipbuf, fp); char b[256]; if (dn_expand(iface->search, iface->search + iface->search_len, iface->search, b, sizeof(b)) > 0) fprintf(fp, "\t%s.%s", c->hostname, b); fprintf(fp, "\t%s\n", c->hostname); md5_hash(ipbuf, strlen(ipbuf), &md5); md5_hash(c->hostname, strlen(c->hostname), &md5); } l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/32 ", ipbuf); leasebuf[l - 1] = '\n'; fwrite(leasebuf, 1, l, fp); } } } fclose(fp); } uint8_t newmd5[16]; md5_end(newmd5, &md5); if (config.dhcp_cb && memcmp(newmd5, statemd5, sizeof(newmd5))) { memcpy(statemd5, newmd5, sizeof(statemd5)); char *argv[2] = {config.dhcp_cb, NULL}; if (!vfork()) { execv(argv[0], argv); _exit(128); } } } static void apply_lease(struct interface *iface, struct dhcpv6_assignment *a, bool add) { if (a->length > 64 || a->managed_size < 0) return; 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) { struct in6_addr prefix = addrs[i].addr; prefix.s6_addr32[1] |= htonl(a->assigned); odhcpd_setup_route(&prefix, (a->managed_size) ? addrs[i].prefix : a->length, iface, &a->peer.sin6_addr, add); } } // More data was received from TCP connection static void managed_handle_pd_data(struct ustream *s, _unused int bytes_new) { struct dhcpv6_assignment *c = container_of(s, struct dhcpv6_assignment, managed_sock); time_t now = odhcpd_time(); bool first = c->managed_size < 0; for (;;) { int pending; char *data = ustream_get_read_buf(s, &pending); char *end = memmem(data, pending, "\n\n", 2); if (!end) break; end += 2; end[-1] = 0; c->managed_size = 0; if (c->accept_reconf) c->reconf_cnt = 1; char *saveptr; for (char *line = strtok_r(data, "\n", &saveptr); line; line = strtok_r(NULL, "\n", &saveptr)) { c->managed = realloc(c->managed, (c->managed_size + 1) * sizeof(*c->managed)); struct odhcpd_ipaddr *n = &c->managed[c->managed_size]; char *saveptr2, *x = strtok_r(line, "/", &saveptr2); if (!x || inet_pton(AF_INET6, x, &n->addr) < 1) continue; x = strtok_r(NULL, ",", &saveptr2); if (sscanf(x, "%hhu", &n->prefix) < 1) continue; x = strtok_r(NULL, ",", &saveptr2); if (sscanf(x, "%u", &n->preferred) < 1) continue; x = strtok_r(NULL, ",", &saveptr2); if (sscanf(x, "%u", &n->valid) < 1) continue; 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; } ustream_consume(s, end - data); } if (first && c->managed_size == 0) free_dhcpv6_assignment(c); } // TCP transmission has ended, either because of success or timeout or other error static void managed_handle_pd_done(struct ustream *s) { struct dhcpv6_assignment *c = container_of(s, struct dhcpv6_assignment, managed_sock); c->valid_until = odhcpd_time() + 15; c->managed_size = 0; if (c->accept_reconf) c->reconf_cnt = 1; } static bool assign_pd(struct interface *iface, struct dhcpv6_assignment *assign) { struct dhcpv6_assignment *c; 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, "%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); } return false; } else if (iface->ia_addr_len < 1) { return false; } // Try honoring the hint first uint32_t current = 1, asize = (1 << (64 - assign->length)) - 1; if (assign->assigned) { list_for_each_entry(c, &iface->ia_assignments, head) { if (c->length == 128 || c->length == 0) continue; if (assign->assigned >= current && assign->assigned + asize < c->assigned) { list_add_tail(&assign->head, &c->head); apply_lease(iface, assign, true); return true; } if (c->assigned != 0) current = (c->assigned + (1 << (64 - c->length))); } } // Fallback to a variable assignment current = 1; list_for_each_entry(c, &iface->ia_assignments, head) { if (c->length == 128 || c->length == 0) continue; current = (current + asize) & (~asize); if (current + asize < c->assigned) { assign->assigned = current; list_add_tail(&assign->head, &c->head); apply_lease(iface, assign, true); return true; } if (c->assigned != 0) current = (c->assigned + (1 << (64 - c->length))); } return false; } static bool assign_na(struct interface *iface, struct dhcpv6_assignment *assign) { bool match = false; for (size_t i = 0; i < iface->ia_addr_len; ++i) { if (!iface->ia_addr[i].has_class) { match = true; continue; } else if (assign->classes_cnt) { for (size_t j = 0; j < assign->classes_cnt; ++j) if (assign->classes[j] == iface->ia_addr[i].class) match = true; } else if (assign->all_class) { match = true; } } if (!match) return false; // Seed RNG with checksum of DUID uint32_t seed = 0; for (size_t i = 0; i < assign->clid_len; ++i) seed += assign->clid_data[i]; srand(seed); // Try to assign up to 100x for (size_t i = 0; i < 100; ++i) { uint32_t try; do try = ((uint32_t)rand()) % 0x0fff; while (try < 0x100); struct dhcpv6_assignment *c; list_for_each_entry(c, &iface->ia_assignments, head) { if (c->length == 0) continue; if (c->assigned > try || c->length != 128) { assign->assigned = try; list_add_tail(&assign->head, &c->head); return true; } else if (c->assigned == try) { break; } } } return false; } static int prefixcmp(const void *va, const void *vb) { const struct odhcpd_ipaddr *a = va, *b = vb; uint32_t a_pref = ((a->addr.s6_addr[0] & 0xfe) != 0xfc) ? a->preferred : 1; uint32_t b_pref = ((b->addr.s6_addr[0] & 0xfe) != 0xfc) ? b->preferred : 1; return (a_pref < b_pref) ? 1 : (a_pref > b_pref) ? -1 : 0; } static void update(struct interface *iface) { struct odhcpd_ipaddr addr[8]; memset(addr, 0, sizeof(addr)); int len = odhcpd_get_interface_addresses(iface->ifindex, addr, 8); if (len < 0) return; qsort(addr, len, sizeof(*addr), prefixcmp); time_t now = odhcpd_time(); int minprefix = -1; for (int i = 0; i < len; ++i) { if (addr[i].prefix > minprefix) minprefix = addr[i].prefix; addr[i].addr.s6_addr32[3] = 0; if (addr[i].preferred < UINT32_MAX - now) addr[i].preferred += now; if (addr[i].valid < UINT32_MAX - now) addr[i].valid += now; } struct dhcpv6_assignment *border = list_last_entry(&iface->ia_assignments, struct dhcpv6_assignment, head); border->assigned = 1 << (64 - minprefix); bool change = len != (int)iface->ia_addr_len; for (int i = 0; !change && i < len; ++i) if (addr[i].addr.s6_addr32[0] != iface->ia_addr[i].addr.s6_addr32[0] || addr[i].addr.s6_addr32[1] != iface->ia_addr[i].addr.s6_addr32[1] || (addr[i].preferred > 0) != (iface->ia_addr[i].preferred > 0) || (addr[i].valid > (uint32_t)now + 7200) != (iface->ia_addr[i].valid > (uint32_t)now + 7200)) change = true; if (change) { struct dhcpv6_assignment *c; list_for_each_entry(c, &iface->ia_assignments, head) if (c != border) apply_lease(iface, c, false); } memcpy(iface->ia_addr, addr, len * sizeof(*addr)); iface->ia_addr_len = len; if (change) { // Addresses / prefixes have changed struct list_head reassign = LIST_HEAD_INIT(reassign); struct dhcpv6_assignment *c, *d; list_for_each_entry_safe(c, d, &iface->ia_assignments, head) { if (c->clid_len == 0 || c->valid_until < now || c->managed_size) continue; if (c->length < 128 && c->assigned >= border->assigned && c != border) list_move(&c->head, &reassign); else if (c != border) apply_lease(iface, c, true); if (c->accept_reconf && c->reconf_cnt == 0) { c->reconf_cnt = 1; c->reconf_sent = now; send_reconf(iface, c); // Leave all other assignments of that client alone struct dhcpv6_assignment *a; list_for_each_entry(a, &iface->ia_assignments, head) if (a != c && a->clid_len == c->clid_len && !memcmp(a->clid_data, c->clid_data, a->clid_len)) c->reconf_cnt = INT_MAX; } } while (!list_empty(&reassign)) { c = list_first_entry(&reassign, struct dhcpv6_assignment, head); list_del(&c->head); if (!assign_pd(iface, c)) { c->assigned = 0; list_add(&c->head, &iface->ia_assignments); } } dhcpv6_write_statefile(); } } static void reconf_timer(struct uloop_timeout *event) { time_t now = odhcpd_time(); struct interface *iface; list_for_each_entry(iface, &interfaces, head) { if (iface->dhcpv6 != RELAYD_SERVER || iface->ia_assignments.next == NULL) continue; struct dhcpv6_assignment *a, *n; list_for_each_entry_safe(a, n, &iface->ia_assignments, head) { if (a->valid_until < now) { if ((a->length < 128 && a->clid_len > 0) || (a->length == 128 && a->clid_len == 0)) { list_del(&a->head); free_dhcpv6_assignment(a); } } else if (a->reconf_cnt > 0 && a->reconf_cnt < 8 && now > a->reconf_sent + (1 << a->reconf_cnt)) { ++a->reconf_cnt; a->reconf_sent = now; send_reconf(iface, a); } } } uloop_timeout_set(event, 2000); } static size_t append_reply(uint8_t *buf, size_t buflen, uint16_t status, const struct dhcpv6_ia_hdr *ia, struct dhcpv6_assignment *a, struct interface *iface, bool request) { if (buflen < sizeof(*ia) + sizeof(struct dhcpv6_ia_prefix)) return 0; struct dhcpv6_ia_hdr out = {ia->type, 0, ia->iaid, 0, 0}; size_t datalen = sizeof(out); time_t now = odhcpd_time(); if (status) { struct __attribute__((packed)) { uint16_t type; uint16_t len; uint16_t value; } stat = {htons(DHCPV6_OPT_STATUS), htons(sizeof(stat) - 4), htons(status)}; memcpy(buf + datalen, &stat, sizeof(stat)); datalen += sizeof(stat); } else { if (a) { uint32_t pref = 3600; uint32_t valid = 3600; 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) { match = false; if (a->classes_cnt) { for (size_t j = 0; j < a->classes_cnt; ++j) if (a->classes[j] == addrs[i].class) match = true; } else if (a->all_class) { match = true; } } if (!match) continue; uint32_t prefix_pref = addrs[i].preferred - now; uint32_t prefix_valid = addrs[i].valid - now; if (addrs[i].prefix > 96 || addrs[i].preferred <= (uint32_t)now) continue; if (prefix_pref > 86400) prefix_pref = 86400; if (prefix_valid > 86400) prefix_valid = 86400; #ifdef DHCPV6_OPT_PREFIX_CLASS struct { uint16_t code; uint16_t length; uint16_t class; } pclass = {htons(DHCPV6_OPT_PREFIX_CLASS), htons(2), htons(iface->ia_addr[i].class)}; #endif if (a->length < 128) { struct dhcpv6_ia_prefix p = { .type = htons(DHCPV6_OPT_IA_PREFIX), .len = htons(sizeof(p) - 4), .preferred = htonl(prefix_pref), .valid = htonl(prefix_valid), .prefix = (a->managed_size) ? addrs[i].prefix : a->length, .addr = addrs[i].addr }; p.addr.s6_addr32[1] |= htonl(a->assigned); size_t entrlen = sizeof(p) - 4; #ifdef DHCPV6_OPT_PREFIX_CLASS if (iface->ia_addr[i].has_class) { entrlen += sizeof(pclass); p.len = htons(entrlen); } #endif if (datalen + entrlen + 4 > buflen || (a->assigned == 0 && a->managed_size == 0)) continue; memcpy(buf + datalen, &p, sizeof(p)); #ifdef DHCPV6_OPT_PREFIX_CLASS memcpy(buf + datalen + sizeof(p), &pclass, sizeof(pclass)); #endif datalen += entrlen + 4; } else { struct dhcpv6_ia_addr n = { .type = htons(DHCPV6_OPT_IA_ADDR), .len = htons(sizeof(n) - 4), .addr = addrs[i].addr, .preferred = htonl(prefix_pref), .valid = htonl(prefix_valid) }; n.addr.s6_addr32[3] = htonl(a->assigned); size_t entrlen = sizeof(n) - 4; #ifdef DHCPV6_OPT_PREFIX_CLASS if (iface->ia_addr[i].has_class) { entrlen += sizeof(pclass); n.len = htons(entrlen); } #endif if (datalen + entrlen + 4 > buflen || a->assigned == 0) continue; memcpy(buf + datalen, &n, sizeof(n)); #ifdef DHCPV6_OPT_PREFIX_CLASS memcpy(buf + datalen + sizeof(n), &pclass, sizeof(pclass)); #endif datalen += entrlen + 4; } // Calculate T1 / T2 based on non-deprecated addresses if (prefix_pref > 0) { if (prefix_pref < pref) pref = prefix_pref; if (prefix_valid < valid) valid = prefix_valid; } } a->valid_until = valid + now; out.t1 = htonl(pref * 5 / 10); out.t2 = htonl(pref * 8 / 10); if (!out.t1) out.t1 = htonl(1); if (!out.t2) out.t2 = htonl(1); } if (!request) { uint8_t *odata, *end = ((uint8_t*)ia) + htons(ia->len) + 4; uint16_t otype, olen; dhcpv6_for_each_option((uint8_t*)&ia[1], end, otype, olen, odata) { struct dhcpv6_ia_prefix *p = (struct dhcpv6_ia_prefix*)&odata[-4]; struct dhcpv6_ia_addr *n = (struct dhcpv6_ia_addr*)&odata[-4]; if ((otype != DHCPV6_OPT_IA_PREFIX || olen < sizeof(*p) - 4) && (otype != DHCPV6_OPT_IA_ADDR || olen < sizeof(*n) - 4)) continue; bool found = false; if (a) { 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) { if (addrs[i].prefix > 96 || addrs[i].preferred <= (uint32_t)now) continue; struct in6_addr addr = addrs[i].addr; if (ia->type == htons(DHCPV6_OPT_IA_PD)) { addr.s6_addr32[1] |= htonl(a->assigned); if (!memcmp(&p->addr, &addr, sizeof(addr)) && p->prefix == a->length) found = true; } else { addr.s6_addr32[3] = htonl(a->assigned); if (!memcmp(&n->addr, &addr, sizeof(addr))) found = true; } } } if (!found) { if (otype == DHCPV6_OPT_IA_PREFIX) { struct dhcpv6_ia_prefix inv = { .type = htons(DHCPV6_OPT_IA_PREFIX), .len = htons(sizeof(inv) - 4), .preferred = 0, .valid = 0, .prefix = p->prefix, .addr = p->addr }; if (datalen + sizeof(inv) > buflen) continue; memcpy(buf + datalen, &inv, sizeof(inv)); datalen += sizeof(inv); } else { struct dhcpv6_ia_addr inv = { .type = htons(DHCPV6_OPT_IA_ADDR), .len = htons(sizeof(inv) - 4), .addr = n->addr, .preferred = 0, .valid = 0 }; if (datalen + sizeof(inv) > buflen) continue; memcpy(buf + datalen, &inv, sizeof(inv)); datalen += sizeof(inv); } } } } } out.len = htons(datalen - 4); memcpy(buf, &out, sizeof(out)); return datalen; } 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(); size_t response_len = 0; const struct dhcpv6_client_header *hdr = data; uint8_t *start = (uint8_t*)&hdr[1], *odata; uint16_t otype, olen; // Find and parse client-id and hostname bool accept_reconf = false; uint8_t *clid_data = NULL, clid_len = 0, mac[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; char hostname[256]; size_t hostname_len = 0; bool class_oro = false; dhcpv6_for_each_option(start, end, otype, olen, odata) { if (otype == DHCPV6_OPT_CLIENTID) { clid_data = odata; clid_len = olen; if (olen == 14 && odata[0] == 0 && odata[1] == 1) memcpy(mac, &odata[8], sizeof(mac)); else if (olen == 10 && odata[0] == 0 && odata[1] == 3) memcpy(mac, &odata[4], sizeof(mac)); } else if (otype == DHCPV6_OPT_FQDN && olen >= 2 && olen <= 255) { uint8_t fqdn_buf[256]; memcpy(fqdn_buf, odata, olen); fqdn_buf[olen++] = 0; if (dn_expand(&fqdn_buf[1], &fqdn_buf[olen], &fqdn_buf[1], hostname, sizeof(hostname)) > 0) hostname_len = strcspn(hostname, "."); } else if (otype == DHCPV6_OPT_ORO) { #ifdef DHCPV6_OPT_PREFIX_CLASS for (size_t i = 0; i + 1 < olen; i += 2) { if (odata[i] == (DHCPV6_OPT_PREFIX_CLASS >> 8) && odata[i + 1] == (DHCPV6_OPT_PREFIX_CLASS & 0xff)) class_oro = true; } #endif } else if (otype == DHCPV6_OPT_RECONF_ACCEPT) { accept_reconf = true; } } if (!clid_data || !clid_len || clid_len > 130) goto out; update(iface); bool update_state = 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); if (!is_pd && !is_na) continue; struct dhcpv6_ia_hdr *ia = (struct dhcpv6_ia_hdr*)&odata[-4]; size_t ia_response_len = 0; uint8_t reqlen = (is_pd) ? 62 : 128; uint32_t reqhint = 0; const uint8_t classes_max = 32; uint8_t classes_cnt = 0; uint16_t classes[classes_max]; // Parse request hint for IA-PD if (is_pd) { uint8_t *sdata; uint16_t stype, slen; dhcpv6_for_each_option(&ia[1], odata + olen, stype, slen, sdata) { if (stype != DHCPV6_OPT_IA_PREFIX || slen < sizeof(struct dhcpv6_ia_prefix) - 4) continue; struct dhcpv6_ia_prefix *p = (struct dhcpv6_ia_prefix*)&sdata[-4]; if (p->prefix) { reqlen = p->prefix; reqhint = ntohl(p->addr.s6_addr32[1]); if (reqlen > 32 && reqlen <= 64) reqhint &= (1U << (64 - reqlen)) - 1; } #ifdef DHCPV6_OPT_PREFIX_CLASS uint8_t *xdata; uint16_t xtype, xlen; dhcpv6_for_each_option(&p[1], sdata + slen, xtype, xlen, xdata) { if (xtype != DHCPV6_OPT_PREFIX_CLASS || xlen != 2) continue; if (classes_cnt >= classes_max) continue; classes[classes_cnt++] = (uint16_t)xdata[0] << 8 | (uint16_t)xdata[1]; } #endif } if (reqlen > 64) reqlen = 64; } else if (is_na) { uint8_t *sdata; uint16_t stype, slen; dhcpv6_for_each_option(&ia[1], odata + olen, stype, slen, sdata) { if (stype != DHCPV6_OPT_IA_ADDR || slen < sizeof(struct dhcpv6_ia_addr) - 4) continue; #ifdef DHCPV6_OPT_PREFIX_CLASS uint8_t *xdata; uint16_t xtype, xlen; struct dhcpv6_ia_addr *p = (struct dhcpv6_ia_addr*)&sdata[-4]; dhcpv6_for_each_option(&p[1], sdata + slen, xtype, xlen, xdata) { if (xtype != DHCPV6_OPT_PREFIX_CLASS || xlen != 2) continue; if (classes_cnt >= classes_max) continue; classes[classes_cnt++] = (uint16_t)xdata[0] << 8 | (uint16_t)xdata[1]; } #endif } } // Find assignment struct dhcpv6_assignment *c, *a = NULL; list_for_each_entry(c, &iface->ia_assignments, head) { if (((c->clid_len == clid_len && !memcmp(c->clid_data, clid_data, clid_len)) || (c->clid_len >= clid_len && !c->clid_data[0] && !c->clid_data[1] && !memcmp(c->mac, mac, sizeof(mac)))) && (c->iaid == ia->iaid || c->valid_until < now) && ((is_pd && c->length <= 64) || (is_na && c->length == 128))) { a = c; // Reset state apply_lease(iface, a, false); memcpy(a->clid_data, clid_data, clid_len); a->clid_len = clid_len; a->iaid = ia->iaid; a->peer = *addr; a->reconf_cnt = 0; a->reconf_sent = 0; a->all_class = class_oro; a->classes_cnt = classes_cnt; a->classes = realloc(a->classes, classes_cnt * sizeof(uint16_t)); if (a->classes) memcpy(a->classes, classes, classes_cnt * sizeof(uint16_t)); break; } } // Generic message handling uint16_t status = DHCPV6_STATUS_OK; if (a && a->managed_size < 0) { return -1; } else if (hdr->msg_type == DHCPV6_MSG_SOLICIT || hdr->msg_type == DHCPV6_MSG_REQUEST) { bool assigned = !!a; if (!a && !iface->no_dynamic_dhcp) { // Create new binding a = calloc(1, sizeof(*a) + clid_len); if (a) { a->clid_len = clid_len; a->iaid = ia->iaid; a->length = reqlen; a->peer = *addr; a->assigned = reqhint; a->all_class = class_oro; a->classes_cnt = classes_cnt; if (classes_cnt) { a->classes = malloc(classes_cnt * sizeof(uint16_t)); if (a->classes) memcpy(a->classes, classes, classes_cnt * sizeof(uint16_t)); } if (first) memcpy(a->key, first->key, sizeof(a->key)); else odhcpd_urandom(a->key, sizeof(a->key)); memcpy(a->clid_data, clid_data, clid_len); if (is_pd) while (!(assigned = assign_pd(iface, a)) && !a->managed_size && ++a->length <= 64); else assigned = assign_na(iface, a); if (a->managed_size && !assigned) return -1; } } if (!assigned || iface->ia_addr_len == 0) { // Set error status status = (is_pd) ? DHCPV6_STATUS_NOPREFIXAVAIL : DHCPV6_STATUS_NOADDRSAVAIL; } else if (assigned && !first) { // size_t handshake_len = 4; buf[0] = 0; buf[1] = DHCPV6_OPT_RECONF_ACCEPT; buf[2] = 0; buf[3] = 0; if (hdr->msg_type == DHCPV6_MSG_REQUEST) { struct dhcpv6_auth_reconfigure auth = { htons(DHCPV6_OPT_AUTH), htons(sizeof(auth) - 4), 3, 1, 0, {htonl(time(NULL)), htonl(++serial)}, 1, {0} }; memcpy(auth.key, a->key, sizeof(a->key)); memcpy(buf + handshake_len, &auth, sizeof(auth)); handshake_len += sizeof(auth); } buf += handshake_len; buflen -= handshake_len; response_len += handshake_len; first = a; } ia_response_len = append_reply(buf, buflen, status, ia, a, iface, true); // Was only a solicitation: mark binding for removal if (assigned && hdr->msg_type == DHCPV6_MSG_SOLICIT) { 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); if (a->hostname) { memcpy(a->hostname, hostname, hostname_len); a->hostname[hostname_len] = 0; } } a->accept_reconf = accept_reconf; apply_lease(iface, a, true); update_state = true; } else if (!assigned && a && a->managed_size == 0) { // Cleanup failed assignment free_dhcpv6_assignment(a); } } else if (hdr->msg_type == DHCPV6_MSG_RENEW || hdr->msg_type == DHCPV6_MSG_RELEASE || hdr->msg_type == DHCPV6_MSG_REBIND || hdr->msg_type == DHCPV6_MSG_DECLINE) { if (!a && hdr->msg_type != DHCPV6_MSG_REBIND) { status = DHCPV6_STATUS_NOBINDING; ia_response_len = append_reply(buf, buflen, status, ia, a, iface, false); } else if (hdr->msg_type == DHCPV6_MSG_RENEW || hdr->msg_type == DHCPV6_MSG_REBIND) { ia_response_len = append_reply(buf, buflen, status, ia, a, iface, false); if (a) apply_lease(iface, a, true); } 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 status = DHCPV6_STATUS_NOTONLINK; ia_response_len = append_reply(buf, buflen, status, ia, a, iface, true); } buf += ia_response_len; buflen -= ia_response_len; response_len += ia_response_len; } if (hdr->msg_type == DHCPV6_MSG_RELEASE && 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; } if (update_state) dhcpv6_write_statefile(); out: return response_len; }