@@ -1326,3 +1326,290 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *buf, size_t buflen, struct interface *ifac
13261326out :
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