|
26 | 26 | #include <unistd.h> |
27 | 27 |
|
28 | 28 | #define DHCP_BUFFER_SIZE 576 |
| 29 | +#define DHCP_MSG_OFFER 2 |
| 30 | +#define DHCP_MSG_ACK 5 |
29 | 31 |
|
30 | 32 | /* Helper function to send netlink message */ |
31 | 33 | static int nl_send(int sock, struct nlmsghdr *nlh) |
@@ -255,6 +257,143 @@ static unsigned char count_leading_ones(uint32_t val) |
255 | 257 | return count; |
256 | 258 | } |
257 | 259 |
|
| 260 | +/* Return the DHCP message type (option 53) from a response, or 0 */ |
| 261 | +static unsigned char get_dhcp_msg_type(const unsigned char *response, |
| 262 | + ssize_t len) |
| 263 | +{ |
| 264 | + /* Walk DHCP options (TLV chain starting after the magic cookie) */ |
| 265 | + size_t p = 240; |
| 266 | + while (p < (size_t)len) { |
| 267 | + unsigned char opt = response[p]; |
| 268 | + |
| 269 | + if (opt == 0xff) /* end */ |
| 270 | + break; |
| 271 | + if (opt == 0) { /* padding */ |
| 272 | + p++; |
| 273 | + continue; |
| 274 | + } |
| 275 | + |
| 276 | + if (p + 1 >= (size_t)len) |
| 277 | + break; |
| 278 | + |
| 279 | + unsigned char opt_len = response[p + 1]; |
| 280 | + p += 2; |
| 281 | + |
| 282 | + if (p + opt_len > (size_t)len) |
| 283 | + break; |
| 284 | + if (opt == 53 && opt_len >= 1) /* Message Type */ |
| 285 | + return response[p]; |
| 286 | + |
| 287 | + p += opt_len; |
| 288 | + } |
| 289 | + return 0; |
| 290 | +} |
| 291 | + |
| 292 | +/* Parse a DHCP ACK and configure the interface. Returns 0 or -1 on error. */ |
| 293 | +static int handle_dhcp_ack(int nl_sock, int iface_index, |
| 294 | + const unsigned char *response, ssize_t len) |
| 295 | +{ |
| 296 | + /* Need at least 240 bytes (DHCP header + magic cookie) + 1 for options */ |
| 297 | + if (len < 241) { |
| 298 | + printf("DHCPACK too short (%zd bytes)\n", len); |
| 299 | + return -1; |
| 300 | + } |
| 301 | + |
| 302 | + /* Parse DHCP response */ |
| 303 | + struct in_addr addr; |
| 304 | + /* yiaddr is at offset 16-19 in network byte order */ |
| 305 | + memcpy(&addr.s_addr, &response[16], sizeof(addr.s_addr)); |
| 306 | + |
| 307 | + if (addr.s_addr == INADDR_ANY) { |
| 308 | + printf("DHCPACK has no address (yiaddr is 0.0.0.0)\n"); |
| 309 | + return -1; |
| 310 | + } |
| 311 | + |
| 312 | + struct in_addr netmask = {.s_addr = INADDR_ANY}; |
| 313 | + struct in_addr router = {.s_addr = INADDR_ANY}; |
| 314 | + /* Clamp MTU to passt's limit */ |
| 315 | + uint16_t mtu = 65520; |
| 316 | + |
| 317 | + FILE *resolv = fopen("/etc/resolv.conf", "w"); |
| 318 | + if (!resolv) { |
| 319 | + perror("Failed to open /etc/resolv.conf"); |
| 320 | + } |
| 321 | + |
| 322 | + /* Parse DHCP options (start at offset 240 after magic cookie) */ |
| 323 | + size_t p = 240; |
| 324 | + while (p < (size_t)len) { |
| 325 | + unsigned char opt = response[p]; |
| 326 | + |
| 327 | + if (opt == 0xff) { |
| 328 | + /* Option 255: End (of options) */ |
| 329 | + break; |
| 330 | + } |
| 331 | + |
| 332 | + if (opt == 0) { /* Padding */ |
| 333 | + p++; |
| 334 | + continue; |
| 335 | + } |
| 336 | + |
| 337 | + if (p + 1 >= (size_t)len) |
| 338 | + break; |
| 339 | + |
| 340 | + unsigned char opt_len = response[p + 1]; |
| 341 | + p += 2; /* Length doesn't include code and length field itself */ |
| 342 | + |
| 343 | + if (p + opt_len > (size_t)len) { |
| 344 | + /* Malformed packet, option length exceeds packet boundary */ |
| 345 | + break; |
| 346 | + } |
| 347 | + |
| 348 | + if (opt == 1 && opt_len >= 4) { |
| 349 | + /* Option 1: Subnet Mask */ |
| 350 | + memcpy(&netmask.s_addr, &response[p], sizeof(netmask.s_addr)); |
| 351 | + } else if (opt == 3 && opt_len >= 4) { |
| 352 | + /* Option 3: Router */ |
| 353 | + memcpy(&router.s_addr, &response[p], sizeof(router.s_addr)); |
| 354 | + } else if (opt == 6 && opt_len >= 4) { |
| 355 | + /* Option 6: Domain Name Server */ |
| 356 | + if (resolv) { |
| 357 | + for (int dns_p = p; dns_p + 4 <= p + opt_len; dns_p += 4) { |
| 358 | + fprintf(resolv, "nameserver %d.%d.%d.%d\n", response[dns_p], |
| 359 | + response[dns_p + 1], response[dns_p + 2], |
| 360 | + response[dns_p + 3]); |
| 361 | + } |
| 362 | + } |
| 363 | + } else if (opt == 26 && opt_len >= 2) { |
| 364 | + /* Option 26: Interface MTU */ |
| 365 | + mtu = (response[p] << 8) | response[p + 1]; |
| 366 | + |
| 367 | + /* We don't know yet if IPv6 is available: don't go below 1280 B |
| 368 | + */ |
| 369 | + if (mtu < 1280) |
| 370 | + mtu = 1280; |
| 371 | + if (mtu > 65520) |
| 372 | + mtu = 65520; |
| 373 | + } |
| 374 | + |
| 375 | + p += opt_len; |
| 376 | + } |
| 377 | + |
| 378 | + if (resolv) { |
| 379 | + fclose(resolv); |
| 380 | + } |
| 381 | + |
| 382 | + /* Calculate prefix length from netmask */ |
| 383 | + unsigned char prefix_len = count_leading_ones(ntohl(netmask.s_addr)); |
| 384 | + |
| 385 | + if (mod_addr4(nl_sock, iface_index, RTM_NEWADDR, addr, prefix_len) != 0) { |
| 386 | + printf("couldn't add the address provided by the DHCP server\n"); |
| 387 | + return -1; |
| 388 | + } |
| 389 | + if (mod_route4(nl_sock, iface_index, RTM_NEWROUTE, router) != 0) { |
| 390 | + printf("couldn't add the default route provided by the DHCP server\n"); |
| 391 | + return -1; |
| 392 | + } |
| 393 | + set_mtu(nl_sock, iface_index, mtu); |
| 394 | + return 0; |
| 395 | +} |
| 396 | + |
258 | 397 | /* Send DISCOVER with Rapid Commit, process ACK, configure address and route */ |
259 | 398 | int do_dhcp(const char *iface) |
260 | 399 | { |
@@ -386,106 +525,90 @@ int do_dhcp(const char *iface) |
386 | 525 | goto cleanup; |
387 | 526 | } |
388 | 527 |
|
389 | | - /* Get and process response (DHCPACK) if any */ |
| 528 | + /* Get response: DHCPACK (Rapid Commit) or DHCPOFFER */ |
390 | 529 | struct sockaddr_in from_addr; |
391 | 530 | socklen_t from_len = sizeof(from_addr); |
392 | 531 | ssize_t len = recvfrom(sock, response, sizeof(response), 0, |
393 | 532 | (struct sockaddr *)&from_addr, &from_len); |
394 | 533 |
|
395 | | - close(sock); |
396 | | - sock = -1; |
| 534 | + if (len <= 0) |
| 535 | + goto done; /* No DHCP response — not an error, VM may be IPv6-only */ |
397 | 536 |
|
398 | | - if (len > 0) { |
399 | | - /* Parse DHCP response */ |
400 | | - struct in_addr addr; |
401 | | - /* yiaddr is at offset 16-19 in network byte order */ |
402 | | - memcpy(&addr.s_addr, &response[16], sizeof(addr.s_addr)); |
| 537 | + unsigned char msg_type = get_dhcp_msg_type(response, len); |
403 | 538 |
|
404 | | - struct in_addr netmask = {.s_addr = INADDR_ANY}; |
405 | | - struct in_addr router = {.s_addr = INADDR_ANY}; |
406 | | - /* Clamp MTU to passt's limit */ |
407 | | - uint16_t mtu = 65520; |
408 | | - |
409 | | - FILE *resolv = fopen("/etc/resolv.conf", "w"); |
410 | | - if (!resolv) { |
411 | | - perror("Failed to open /etc/resolv.conf"); |
| 539 | + if (msg_type == DHCP_MSG_ACK) { |
| 540 | + /* Rapid Commit — server sent ACK directly */ |
| 541 | + close(sock); |
| 542 | + sock = -1; |
| 543 | + if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0) |
| 544 | + goto cleanup; |
| 545 | + } else if (msg_type == DHCP_MSG_OFFER) { |
| 546 | + /* |
| 547 | + * DHCPOFFER — complete the 4-way handshake by sending DHCPREQUEST |
| 548 | + * and waiting for DHCPACK. Servers without Rapid Commit (e.g. |
| 549 | + * gvproxy) require this. |
| 550 | + */ |
| 551 | + struct in_addr offered_addr; |
| 552 | + memcpy(&offered_addr.s_addr, &response[16], |
| 553 | + sizeof(offered_addr.s_addr)); |
| 554 | + |
| 555 | + /* Build DHCPREQUEST */ |
| 556 | + memset(request.options, 0, sizeof(request.options)); |
| 557 | + opt_offset = 0; |
| 558 | + |
| 559 | + /* Option 53: DHCP Message Type = REQUEST (3) */ |
| 560 | + request.options[opt_offset++] = 53; |
| 561 | + request.options[opt_offset++] = 1; |
| 562 | + request.options[opt_offset++] = 3; |
| 563 | + |
| 564 | + /* Option 50: Requested IP Address */ |
| 565 | + request.options[opt_offset++] = 50; |
| 566 | + request.options[opt_offset++] = 4; |
| 567 | + memcpy(&request.options[opt_offset], &offered_addr.s_addr, 4); |
| 568 | + opt_offset += 4; |
| 569 | + |
| 570 | + /* Option 54: Server Identifier (from_addr) */ |
| 571 | + request.options[opt_offset++] = 54; |
| 572 | + request.options[opt_offset++] = 4; |
| 573 | + memcpy(&request.options[opt_offset], &from_addr.sin_addr.s_addr, 4); |
| 574 | + opt_offset += 4; |
| 575 | + |
| 576 | + /* Option 255: End */ |
| 577 | + request.options[opt_offset++] = 0xff; |
| 578 | + |
| 579 | + if (sendto(sock, &request, sizeof(request), 0, |
| 580 | + (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) { |
| 581 | + perror("sendto DHCPREQUEST failed"); |
| 582 | + goto cleanup; |
412 | 583 | } |
413 | 584 |
|
414 | | - /* Parse DHCP options (start at offset 240 after magic cookie) */ |
415 | | - size_t p = 240; |
416 | | - while (p < (size_t)len) { |
417 | | - unsigned char opt = response[p]; |
| 585 | + from_len = sizeof(from_addr); |
| 586 | + len = recvfrom(sock, response, sizeof(response), 0, |
| 587 | + (struct sockaddr *)&from_addr, &from_len); |
418 | 588 |
|
419 | | - if (opt == 0xff) { |
420 | | - /* Option 255: End (of options) */ |
421 | | - break; |
422 | | - } |
423 | | - |
424 | | - if (opt == 0) { /* Padding */ |
425 | | - p++; |
426 | | - continue; |
427 | | - } |
428 | | - |
429 | | - unsigned char opt_len = response[p + 1]; |
430 | | - p += 2; /* Length doesn't include code and length field itself */ |
431 | | - |
432 | | - if (p + opt_len > (size_t)len) { |
433 | | - /* Malformed packet, option length exceeds packet boundary */ |
434 | | - break; |
435 | | - } |
436 | | - |
437 | | - if (opt == 1) { |
438 | | - /* Option 1: Subnet Mask */ |
439 | | - memcpy(&netmask.s_addr, &response[p], sizeof(netmask.s_addr)); |
440 | | - } else if (opt == 3) { |
441 | | - /* Option 3: Router */ |
442 | | - memcpy(&router.s_addr, &response[p], sizeof(router.s_addr)); |
443 | | - } else if (opt == 6) { |
444 | | - /* Option 6: Domain Name Server */ |
445 | | - if (resolv) { |
446 | | - for (int dns_p = p; dns_p + 3 < p + opt_len; dns_p += 4) { |
447 | | - fprintf(resolv, "nameserver %d.%d.%d.%d\n", |
448 | | - response[dns_p], response[dns_p + 1], |
449 | | - response[dns_p + 2], response[dns_p + 3]); |
450 | | - } |
451 | | - } |
452 | | - } else if (opt == 26) { |
453 | | - /* Option 26: Interface MTU */ |
454 | | - mtu = (response[p] << 8) | response[p + 1]; |
455 | | - |
456 | | - /* We don't know yet if IPv6 is available: don't go below 1280 B |
457 | | - */ |
458 | | - if (mtu < 1280) |
459 | | - mtu = 1280; |
460 | | - if (mtu > 65520) |
461 | | - mtu = 65520; |
462 | | - } |
463 | | - |
464 | | - p += opt_len; |
465 | | - } |
| 589 | + close(sock); |
| 590 | + sock = -1; |
466 | 591 |
|
467 | | - if (resolv) { |
468 | | - fclose(resolv); |
| 592 | + if (len <= 0) { |
| 593 | + printf("no DHCPACK received\n"); |
| 594 | + goto cleanup; |
469 | 595 | } |
470 | 596 |
|
471 | | - /* Calculate prefix length from netmask */ |
472 | | - unsigned char prefix_len = count_leading_ones(ntohl(netmask.s_addr)); |
473 | | - |
474 | | - if (mod_addr4(nl_sock, iface_index, RTM_NEWADDR, addr, prefix_len) != |
475 | | - 0) { |
476 | | - printf("couldn't add the address provided by the DHCP server\n"); |
| 597 | + if (get_dhcp_msg_type(response, len) != DHCP_MSG_ACK) { |
| 598 | + printf("expected DHCPACK but got message type %d\n", |
| 599 | + get_dhcp_msg_type(response, len)); |
477 | 600 | goto cleanup; |
478 | 601 | } |
479 | | - if (mod_route4(nl_sock, iface_index, RTM_NEWROUTE, router) != 0) { |
480 | | - printf( |
481 | | - "couldn't add the default route provided by the DHCP server\n"); |
| 602 | + |
| 603 | + if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0) |
482 | 604 | goto cleanup; |
483 | | - } |
484 | | - set_mtu(nl_sock, iface_index, mtu); |
| 605 | + } else { |
| 606 | + printf("unexpected DHCP message type %d\n", msg_type); |
| 607 | + goto cleanup; |
485 | 608 | } |
486 | 609 |
|
| 610 | +done: |
487 | 611 | ret = 0; |
488 | | - |
489 | 612 | cleanup: |
490 | 613 | if (sock >= 0) { |
491 | 614 | close(sock); |
|
0 commit comments