Skip to content

Commit 6937e84

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 5dea689 commit 6937e84

2 files changed

Lines changed: 210 additions & 85 deletions

File tree

init/dhcp.c

Lines changed: 204 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,143 @@ 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+
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+
258397
/* Send DISCOVER with Rapid Commit, process ACK, configure address and route */
259398
int do_dhcp(const char *iface)
260399
{
@@ -386,106 +525,90 @@ int do_dhcp(const char *iface)
386525
goto cleanup;
387526
}
388527

389-
/* Get and process response (DHCPACK) if any */
528+
/* Get response: DHCPACK (Rapid Commit) or DHCPOFFER */
390529
struct sockaddr_in from_addr;
391530
socklen_t from_len = sizeof(from_addr);
392531
ssize_t len = recvfrom(sock, response, sizeof(response), 0,
393532
(struct sockaddr *)&from_addr, &from_len);
394533

395-
close(sock);
396-
sock = -1;
534+
if (len <= 0)
535+
goto done; /* No DHCP response — not an error, VM may be IPv6-only */
397536

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);
403538

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;
412583
}
413584

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);
418588

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;
466591

467-
if (resolv) {
468-
fclose(resolv);
592+
if (len <= 0) {
593+
printf("no DHCPACK received\n");
594+
goto cleanup;
469595
}
470596

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));
477600
goto cleanup;
478601
}
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)
482604
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;
485608
}
486609

610+
done:
487611
ret = 0;
488-
489612
cleanup:
490613
if (sock >= 0) {
491614
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)