Skip to content

Commit d640b56

Browse files
mtjhrcslp
authored andcommitted
init: Implement fallback for DHCP servers without Rapid Commit
When a server answers DHCPDISCOVER with DHCPOFFER instead of an immediate ACK, send DHCPREQUEST for the and wait for the final ACK. This makes DHCP work on macOS hosts when using gvproxy for networking. Signed-off-by: Matej Hrica <mhrica@redhat.com>
1 parent 17b43db commit d640b56

2 files changed

Lines changed: 204 additions & 85 deletions

File tree

init/dhcp.c

Lines changed: 198 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
#include <unistd.h>
2727

2828
#define DHCP_BUFFER_SIZE 576
29+
#define DHCP_MSG_OFFER 2
30+
#define DHCP_MSG_ACK 5
2931

3032
/* Helper function to send netlink message */
3133
static int nl_send(int sock, struct nlmsghdr *nlh)
@@ -255,6 +257,137 @@ static unsigned char count_leading_ones(uint32_t val)
255257
return count;
256258
}
257259

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+
unsigned char opt_len = response[p + 1];
277+
p += 2;
278+
279+
if (p + opt_len > (size_t)len)
280+
break;
281+
if (opt == 53 && opt_len >= 1) /* Message Type */
282+
return response[p];
283+
284+
p += opt_len;
285+
}
286+
return 0;
287+
}
288+
289+
/* Parse a DHCP ACK and configure the interface. Returns 0 or -1 on error. */
290+
static int handle_dhcp_ack(int nl_sock, int iface_index,
291+
const unsigned char *response, ssize_t len)
292+
{
293+
/* Need at least 240 bytes (DHCP header + magic cookie) + 1 for options */
294+
if (len < 241) {
295+
printf("DHCPACK too short (%zd bytes)\n", len);
296+
return -1;
297+
}
298+
299+
/* Parse DHCP response */
300+
struct in_addr addr;
301+
/* yiaddr is at offset 16-19 in network byte order */
302+
memcpy(&addr.s_addr, &response[16], sizeof(addr.s_addr));
303+
304+
if (addr.s_addr == INADDR_ANY) {
305+
printf("DHCPACK has no address (yiaddr is 0.0.0.0)\n");
306+
return -1;
307+
}
308+
309+
struct in_addr netmask = {.s_addr = INADDR_ANY};
310+
struct in_addr router = {.s_addr = INADDR_ANY};
311+
/* Clamp MTU to passt's limit */
312+
uint16_t mtu = 65520;
313+
314+
FILE *resolv = fopen("/etc/resolv.conf", "w");
315+
if (!resolv) {
316+
perror("Failed to open /etc/resolv.conf");
317+
}
318+
319+
/* Parse DHCP options (start at offset 240 after magic cookie) */
320+
size_t p = 240;
321+
while (p < (size_t)len) {
322+
unsigned char opt = response[p];
323+
324+
if (opt == 0xff) {
325+
/* Option 255: End (of options) */
326+
break;
327+
}
328+
329+
if (opt == 0) { /* Padding */
330+
p++;
331+
continue;
332+
}
333+
334+
unsigned char opt_len = response[p + 1];
335+
p += 2; /* Length doesn't include code and length field itself */
336+
337+
if (p + opt_len > (size_t)len) {
338+
/* Malformed packet, option length exceeds packet boundary */
339+
break;
340+
}
341+
342+
if (opt == 1 && opt_len >= 4) {
343+
/* Option 1: Subnet Mask */
344+
memcpy(&netmask.s_addr, &response[p], sizeof(netmask.s_addr));
345+
} else if (opt == 3 && opt_len >= 4) {
346+
/* Option 3: Router */
347+
memcpy(&router.s_addr, &response[p], sizeof(router.s_addr));
348+
} else if (opt == 6 && opt_len >= 4) {
349+
/* Option 6: Domain Name Server */
350+
if (resolv) {
351+
for (int dns_p = p; dns_p + 4 <= p + opt_len; dns_p += 4) {
352+
fprintf(resolv, "nameserver %d.%d.%d.%d\n", response[dns_p],
353+
response[dns_p + 1], response[dns_p + 2],
354+
response[dns_p + 3]);
355+
}
356+
}
357+
} else if (opt == 26 && opt_len >= 2) {
358+
/* Option 26: Interface MTU */
359+
mtu = (response[p] << 8) | response[p + 1];
360+
361+
/* We don't know yet if IPv6 is available: don't go below 1280 B
362+
*/
363+
if (mtu < 1280)
364+
mtu = 1280;
365+
if (mtu > 65520)
366+
mtu = 65520;
367+
}
368+
369+
p += opt_len;
370+
}
371+
372+
if (resolv) {
373+
fclose(resolv);
374+
}
375+
376+
/* Calculate prefix length from netmask */
377+
unsigned char prefix_len = count_leading_ones(ntohl(netmask.s_addr));
378+
379+
if (mod_addr4(nl_sock, iface_index, RTM_NEWADDR, addr, prefix_len) != 0) {
380+
printf("couldn't add the address provided by the DHCP server\n");
381+
return -1;
382+
}
383+
if (mod_route4(nl_sock, iface_index, RTM_NEWROUTE, router) != 0) {
384+
printf("couldn't add the default route provided by the DHCP server\n");
385+
return -1;
386+
}
387+
set_mtu(nl_sock, iface_index, mtu);
388+
return 0;
389+
}
390+
258391
/* Send DISCOVER with Rapid Commit, process ACK, configure address and route */
259392
int do_dhcp(const char *iface)
260393
{
@@ -386,106 +519,90 @@ int do_dhcp(const char *iface)
386519
goto cleanup;
387520
}
388521

389-
/* Get and process response (DHCPACK) if any */
522+
/* Get response: DHCPACK (Rapid Commit) or DHCPOFFER */
390523
struct sockaddr_in from_addr;
391524
socklen_t from_len = sizeof(from_addr);
392525
ssize_t len = recvfrom(sock, response, sizeof(response), 0,
393526
(struct sockaddr *)&from_addr, &from_len);
394527

395-
close(sock);
396-
sock = -1;
397-
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));
528+
if (len <= 0)
529+
goto done; /* No DHCP response — not an error, VM may be IPv6-only */
403530

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;
531+
unsigned char msg_type = get_dhcp_msg_type(response, len);
408532

409-
FILE *resolv = fopen("/etc/resolv.conf", "w");
410-
if (!resolv) {
411-
perror("Failed to open /etc/resolv.conf");
533+
if (msg_type == DHCP_MSG_ACK) {
534+
/* Rapid Commit — server sent ACK directly */
535+
close(sock);
536+
sock = -1;
537+
if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0)
538+
goto cleanup;
539+
} else if (msg_type == DHCP_MSG_OFFER) {
540+
/*
541+
* DHCPOFFER — complete the 4-way handshake by sending DHCPREQUEST
542+
* and waiting for DHCPACK. Servers without Rapid Commit (e.g.
543+
* gvproxy) require this.
544+
*/
545+
struct in_addr offered_addr;
546+
memcpy(&offered_addr.s_addr, &response[16],
547+
sizeof(offered_addr.s_addr));
548+
549+
/* Build DHCPREQUEST */
550+
memset(request.options, 0, sizeof(request.options));
551+
opt_offset = 0;
552+
553+
/* Option 53: DHCP Message Type = REQUEST (3) */
554+
request.options[opt_offset++] = 53;
555+
request.options[opt_offset++] = 1;
556+
request.options[opt_offset++] = 3;
557+
558+
/* Option 50: Requested IP Address */
559+
request.options[opt_offset++] = 50;
560+
request.options[opt_offset++] = 4;
561+
memcpy(&request.options[opt_offset], &offered_addr.s_addr, 4);
562+
opt_offset += 4;
563+
564+
/* Option 54: Server Identifier (from_addr) */
565+
request.options[opt_offset++] = 54;
566+
request.options[opt_offset++] = 4;
567+
memcpy(&request.options[opt_offset], &from_addr.sin_addr.s_addr, 4);
568+
opt_offset += 4;
569+
570+
/* Option 255: End */
571+
request.options[opt_offset++] = 0xff;
572+
573+
if (sendto(sock, &request, sizeof(request), 0,
574+
(struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
575+
perror("sendto DHCPREQUEST failed");
576+
goto cleanup;
412577
}
413578

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];
579+
from_len = sizeof(from_addr);
580+
len = recvfrom(sock, response, sizeof(response), 0,
581+
(struct sockaddr *)&from_addr, &from_len);
418582

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-
}
583+
close(sock);
584+
sock = -1;
466585

467-
if (resolv) {
468-
fclose(resolv);
586+
if (len <= 0) {
587+
printf("no DHCPACK received\n");
588+
goto cleanup;
469589
}
470590

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");
591+
if (get_dhcp_msg_type(response, len) != DHCP_MSG_ACK) {
592+
printf("expected DHCPACK but got message type %d\n",
593+
get_dhcp_msg_type(response, len));
477594
goto cleanup;
478595
}
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");
596+
597+
if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0)
482598
goto cleanup;
483-
}
484-
set_mtu(nl_sock, iface_index, mtu);
599+
} else {
600+
printf("unexpected DHCP message type %d\n", msg_type);
601+
goto cleanup;
485602
}
486603

604+
done:
487605
ret = 0;
488-
489606
cleanup:
490607
if (sock >= 0) {
491608
close(sock);

init/dhcp.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@ struct dhcp_packet {
3737
* Perform DHCP discovery and configuration for a network interface
3838
*
3939
* This function:
40-
* 1. Sets up a temporary link-local address (169.254.1.1/16)
40+
* 1. Binds a UDP socket to the interface using SO_BINDTODEVICE
4141
* 2. Sends a DHCP DISCOVER message with Rapid Commit option
42-
* 3. Waits up to 100ms for a DHCP ACK response
43-
* 4. Parses the response and configures:
42+
* 3. Waits up to 100ms for a response:
43+
* - If DHCPACK (Rapid Commit): applies configuration directly
44+
* - If DHCPOFFER: sends DHCPREQUEST and waits for DHCPACK
45+
* - If no response: returns success (VM may be IPv6-only)
46+
* 4. Parses the ACK and configures:
4447
* - IPv4 address with appropriate prefix length
4548
* - Default gateway route
4649
* - DNS servers (overwriting /etc/resolv.conf)
4750
* - Interface MTU
48-
* 5. Cleans up temporary configuration
4951
*
5052
* Parameters:
5153
* iface - The name of the network interface to be configured.

0 commit comments

Comments
 (0)