Skip to content

Commit 7499b52

Browse files
committed
dhcpv6: implement RFC9686
Registering Self-Generated IPv6 Addresses Using DHCPv6 This functionality registers a client SLAAC or static address in the DHCPv6 pool, primarily for diagnostic purposes as outlined in §1. The address registration mechanism overview: ``` +--------+ +------------------+ +---------------+ | CLIENT | | FIRST-HOP ROUTER | | DHCPv6 SERVER | +--------+ +---------+--------+ +-------+-------+ | SLAAC | | |<--------------------> | | | | | | | | src: link-local address | | --------------------------------------------> | | INFORMATION-REQUEST or SOLICIT/... | | - OPTION REQUEST OPTION | | -- OPTION_ADDR_REG_ENABLE | | | | ... | | | | | |<--------------------------------------------- | | REPLY or ADVERTISE MESSAGE | | - OPTION_ADDR_REG_ENABLE | | | | | | src: address being registered | | --------------------------------------------> | | ADDR-REG-INFORM MESSAGE |Register/ | |log addresses | | | | | <-------------------------------------------- | | ADDR-REG-REPLY MESSAGE | | | ``` Signed-off-by: Paul Donald <newtwen+github@gmail.com> Link: openwrt#353
1 parent 3fda5f8 commit 7499b52

File tree

7 files changed

+432
-5
lines changed

7 files changed

+432
-5
lines changed

src/dhcpv6-ia.c

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,3 +1326,290 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *buf, size_t buflen, struct interface *ifac
13261326
out:
13271327
return response_len;
13281328
}
1329+
1330+
/* Register or update address registration in the lease database */
1331+
static bool register_ia_addr_in_lease_db(struct sockaddr_in6 *source,
1332+
const uint8_t *clientid_data, uint16_t clientid_len,
1333+
const struct dhcpv6_ia_addr *ia_addr,
1334+
struct interface *iface)
1335+
{
1336+
/* RFC9686 §4.2.1: Lease lifetime is the valid-lifetime from IA Address option */
1337+
time_t now = odhcpd_time();
1338+
uint32_t valid_lt = ntohl(ia_addr->valid_lt);
1339+
time_t lease_end = now + valid_lt;
1340+
bool onlink = false;
1341+
/* variables for logging */
1342+
char addrbuf[INET6_ADDRSTRLEN];
1343+
inet_ntop(AF_INET6, &ia_addr->addr, addrbuf, sizeof(addrbuf));
1344+
char duidbuf[DUID_HEXSTRLEN] = {0};
1345+
const char *duidstr = duidbuf;
1346+
1347+
/* Search for existing binding with this address */
1348+
struct dhcpv6_lease *binding = NULL, *lease = NULL;
1349+
list_for_each_entry(binding, &iface->ia_assignments, head) {
1350+
/* Check if this address is already registered by another client */
1351+
if ((binding->flags & OAF_DHCPV6_ADDR_REG) &&
1352+
memcmp(&binding->peer.sin6_addr, &ia_addr->addr, sizeof(struct in6_addr)) == 0) {
1353+
/* Found address registration for this address */
1354+
if (binding->duid_len == clientid_len &&
1355+
memcmp(binding->duid, clientid_data, clientid_len) == 0) {
1356+
/* Same client, update the lease */
1357+
lease = binding;
1358+
break;
1359+
} else {
1360+
/* RFC9686 §4.2.1: Different client, should log and update binding
1361+
* We'll update to the new client */
1362+
1363+
if (!binding->duid_len || binding->duid_len > DUID_MAX_LEN)
1364+
duidstr = "<invalid-duid>";
1365+
else
1366+
odhcpd_hexlify(duidbuf, binding->duid, binding->duid_len);
1367+
1368+
notice("ADDR-REG-INFORM: address collision for %s: was bound to "
1369+
"different client DUID(%s); updating", addrbuf, duidstr);
1370+
lease = binding;
1371+
break;
1372+
}
1373+
}
1374+
}
1375+
1376+
if (!lease) {
1377+
/* Create new lease for this address registration */
1378+
lease = dhcpv6_alloc_lease(clientid_len);
1379+
if (!lease) {
1380+
error("ADDR-REG-INFORM: failed to allocate lease for %s", addrbuf);
1381+
return false;
1382+
}
1383+
1384+
/* Initialize lease structure */
1385+
lease->iface = iface;
1386+
lease->peer = *source;
1387+
lease->peer.sin6_addr = ia_addr->addr; /* Store full registered address */
1388+
lease->duid_len = clientid_len;
1389+
memcpy(lease->duid, clientid_data, clientid_len);
1390+
lease->length = 128;
1391+
1392+
/* Try to find matching interface prefix and extract host ID */
1393+
for (size_t i = 0; i < iface->addr6_len; i++) {
1394+
if (!valid_addr(&iface->addr6[i], now))
1395+
continue;
1396+
1397+
if (ADDR_MATCH_PIO_FILTER(&iface->addr6[i], iface))
1398+
continue;
1399+
1400+
/* RFC9686 §4.2.1: the server SHOULD verify that the address
1401+
* is "appropriate to the link" */
1402+
if (odhcpd_bmemcmp(&ia_addr->addr, &iface->addr6[i].addr.in6,
1403+
iface->addr6[i].prefix_len) == 0) {
1404+
/* Address is within this prefix - extract host ID portion */
1405+
onlink = true;
1406+
uint64_t host_id = 0;
1407+
memcpy(&host_id, &ia_addr->addr.s6_addr[8], 8);
1408+
lease->assigned_host_id = be64toh(host_id);
1409+
break;
1410+
}
1411+
}
1412+
1413+
if (onlink) {
1414+
/* Add to interface's lease list */
1415+
list_add(&lease->head, &iface->ia_assignments);
1416+
} else {
1417+
/* RFC9686 §4.2.1: If the address fails verification, the server SHOULD log this fact */
1418+
notice("ADDR-REG-INFORM: address %s not on this link %s", addrbuf, iface->name);
1419+
dhcpv6_free_lease(lease);
1420+
lease = NULL;
1421+
return onlink;
1422+
}
1423+
1424+
} else {
1425+
/* Update existing lease */
1426+
lease->peer = *source;
1427+
lease->peer.sin6_addr = ia_addr->addr; /* Update registered address */
1428+
lease->duid_len = clientid_len;
1429+
memcpy(lease->duid, clientid_data, clientid_len);
1430+
}
1431+
1432+
/* Update lifetimes */
1433+
lease->valid_until = lease_end;
1434+
lease->preferred_until = now + ntohl(ia_addr->preferred_lt);
1435+
lease->bound = true;
1436+
1437+
/* Mark flags - RFC9686 Address Registration lease */
1438+
lease->flags = OAF_DHCPV6_ADDR_REG;
1439+
1440+
1441+
if (!lease->duid_len || lease->duid_len > DUID_MAX_LEN)
1442+
duidstr = "<invalid-duid>";
1443+
else
1444+
odhcpd_hexlify(duidbuf, lease->duid, lease->duid_len);
1445+
1446+
info("ADDR-REG-INFORM: registered %s for client with DUID %s, "
1447+
"valid until %ld (in %u seconds)", addrbuf, duidstr,
1448+
lease_end, valid_lt);
1449+
1450+
return true;
1451+
}
1452+
1453+
/* Send RFC9686 ADDR-REG-REPLY message to client */
1454+
static void send_ia_addr_reg_reply(struct sockaddr_in6 *source,
1455+
const struct dhcpv6_client_header *hdr,
1456+
const uint8_t *clientid_data,
1457+
uint16_t clientid_len,
1458+
const struct dhcpv6_ia_addr *ia_addr,
1459+
struct interface *iface)
1460+
{
1461+
/* RFC9686 §4.3: Reply MUST contain:
1462+
* - msg_type: ADDR-REG-REPLY (37)
1463+
* - transaction_id: copied from ADDR-REG-INFORM
1464+
* - IA Address option: identical to the one in the request
1465+
*/
1466+
1467+
struct {
1468+
uint8_t msg_type;
1469+
uint8_t tr_id[3];
1470+
} _o_packed reply = {
1471+
.msg_type = DHCPV6_MSG_ADDR_REG_REPLY,
1472+
};
1473+
/* Copy transaction ID from request */
1474+
memcpy(reply.tr_id, hdr->transaction_id, sizeof(reply.tr_id));
1475+
1476+
struct {
1477+
uint16_t code;
1478+
uint16_t len;
1479+
uint8_t data[DUID_MAX_LEN];
1480+
} _o_packed serverid = {
1481+
.code = htons(DHCPV6_OPT_SERVERID),
1482+
.len = 0,
1483+
.data = { 0 },
1484+
};
1485+
struct {
1486+
uint16_t code;
1487+
uint16_t len;
1488+
uint8_t data[DUID_MAX_LEN];
1489+
} _o_packed clientid = {
1490+
.code = htons(DHCPV6_OPT_CLIENTID),
1491+
.len = htons(clientid_len),
1492+
.data = { 0 },
1493+
};
1494+
memcpy(clientid.data, clientid_data, clientid_len);
1495+
1496+
if (config.default_duid_len > 0) {
1497+
memcpy(serverid.data, config.default_duid, config.default_duid_len);
1498+
serverid.len = htons(config.default_duid_len);
1499+
} else {
1500+
uint16_t duid_ll_hdr[] = {
1501+
htons(DUID_TYPE_LL),
1502+
htons(ARPHRD_ETHER)
1503+
};
1504+
memcpy(serverid.data, duid_ll_hdr, sizeof(duid_ll_hdr));
1505+
odhcpd_get_mac(iface, &serverid.data[sizeof(duid_ll_hdr)]);
1506+
serverid.len = htons(sizeof(duid_ll_hdr) + ETH_ALEN);
1507+
}
1508+
1509+
struct iovec iov[] = {
1510+
{ &reply, sizeof(reply) },
1511+
{ &serverid, sizeof(serverid) },
1512+
{ &clientid, sizeof(clientid) },
1513+
{ (void *)ia_addr, sizeof(struct dhcpv6_ia_addr) },
1514+
};
1515+
1516+
size_t serverid_len, clientid_opt_len;
1517+
1518+
serverid_len = sizeof(serverid.code) + sizeof(serverid.len) + ntohs(serverid.len);
1519+
clientid_opt_len = sizeof(clientid.code) + sizeof(clientid.len) + clientid_len;
1520+
1521+
iov[1].iov_len = serverid_len;
1522+
iov[2].iov_len = clientid_opt_len;
1523+
1524+
/* RFC9686 §4.3: If not relayed, destination is the address being registered.
1525+
* If relayed, we would construct Relay-reply (handled separately in relay_server_response).
1526+
* For direct replies, source is already the registered address. */
1527+
1528+
char addrbuf[INET6_ADDRSTRLEN];
1529+
inet_ntop(AF_INET6, &ia_addr->addr, addrbuf, sizeof(addrbuf));
1530+
info("Sending ADDR-REG-REPLY for %s on %s", addrbuf, iface->name);
1531+
1532+
odhcpd_send(iface->dhcpv6_event.uloop.fd, source, iov, ARRAY_SIZE(iov), iface);
1533+
}
1534+
1535+
/* RFC9686 Address Registration Message Handler */
1536+
void handle_ia_addr_reg_inform(struct sockaddr_in6 *source,
1537+
const void *data, size_t len, struct interface *iface)
1538+
{
1539+
const struct dhcpv6_client_header *hdr = data;
1540+
uint8_t *opts = (uint8_t *)&hdr[1], *opts_end = (uint8_t *)data + len;
1541+
uint16_t otype, olen;
1542+
uint8_t *odata;
1543+
1544+
uint8_t *clientid_data = NULL;
1545+
uint16_t clientid_len = 0;
1546+
struct dhcpv6_ia_addr *ia_addr = NULL;
1547+
1548+
if (len < sizeof(*hdr)) {
1549+
error("ADDR-REG-INFORM: message too short");
1550+
return;
1551+
}
1552+
1553+
/* RFC9686 §4.2: Client MUST include Client Identifier option */
1554+
/* RFC9686 §4.2: ADDR-REG-INFORM MUST NOT contain Server Identifier */
1555+
/* RFC9686 §4.2: MUST contain exactly one IA Address option */
1556+
1557+
dhcpv6_for_each_option(opts, opts_end, otype, olen, odata) {
1558+
switch (otype) {
1559+
case DHCPV6_OPT_CLIENTID:
1560+
if (olen > 0) {
1561+
clientid_data = odata;
1562+
clientid_len = olen;
1563+
}
1564+
break;
1565+
case DHCPV6_OPT_SERVERID:
1566+
/* RFC9686 §4.2: Server MUST discard messages with Server ID */
1567+
notice("ADDR-REG-INFORM: message contains Server Identifier, discarding");
1568+
return;
1569+
case DHCPV6_OPT_IA_ADDR:
1570+
if (olen == sizeof(struct dhcpv6_ia_addr) - 4) {
1571+
if (ia_addr != NULL) {
1572+
debug("ADDR-REG-INFORM: message contains more than one IA_ADDR, discarding");
1573+
return;
1574+
}
1575+
ia_addr = (struct dhcpv6_ia_addr *)&odata[-4];
1576+
}
1577+
break;
1578+
case DHCPV6_OPT_ORO:
1579+
/* RFC9686 §4.2: ADDR-REG-INFORM MUST NOT contain Option Request option */
1580+
notice("ADDR-REG-INFORM: message contains Option Request, discarding");
1581+
return;
1582+
default:
1583+
break;
1584+
}
1585+
}
1586+
1587+
/* Validate message contents */
1588+
if (!clientid_data || clientid_len == 0) {
1589+
notice("ADDR-REG-INFORM: missing Client Identifier option, discarding");
1590+
return;
1591+
}
1592+
1593+
if (!ia_addr) {
1594+
notice("ADDR-REG-INFORM: missing or invalid IA Address option, discarding");
1595+
return;
1596+
}
1597+
1598+
/* RFC9686 §4.2.1: Verify source address matches the IA Address option
1599+
* The message MUST be sent from the address being registered */
1600+
if (memcmp(&source->sin6_addr, &ia_addr->addr, sizeof(struct in6_addr)) != 0) {
1601+
notice("ADDR-REG-INFORM: source address does not match IA Address option");
1602+
return;
1603+
}
1604+
1605+
char addrbuf[INET6_ADDRSTRLEN];
1606+
inet_ntop(AF_INET6, &ia_addr->addr, addrbuf, sizeof(addrbuf));
1607+
debug("Got ADDR-REG-INFORM for %s on %s", addrbuf, iface->name);
1608+
1609+
/* Register address in lease database */
1610+
if(!register_ia_addr_in_lease_db(source, clientid_data, clientid_len, ia_addr, iface))
1611+
return;
1612+
1613+
/* Send ADDR-REG-REPLY response */
1614+
send_ia_addr_reg_reply(source, hdr, clientid_data, clientid_len, ia_addr, iface);
1615+
}

0 commit comments

Comments
 (0)