Skip to content

Commit bae9cc7

Browse files
committed
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 a181718 commit bae9cc7

1 file changed

Lines changed: 184 additions & 82 deletions

File tree

init/dhcp.c

Lines changed: 184 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
#include <unistd.h>
2626

2727
#define DHCP_BUFFER_SIZE 576
28+
#define DHCP_MSG_OFFER 2
29+
#define DHCP_MSG_ACK 5
2830

2931
/* Helper function to send netlink message */
3032
static int nl_send(int sock, struct nlmsghdr *nlh)
@@ -254,6 +256,127 @@ static unsigned char count_leading_ones(uint32_t val)
254256
return count;
255257
}
256258

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

379-
/* Get and process response (DHCPACK) if any */
502+
/* Get response: DHCPACK (Rapid Commit) or DHCPOFFER */
380503
struct sockaddr_in from_addr;
381504
socklen_t from_len = sizeof(from_addr);
382505
ssize_t len = recvfrom(sock, response, sizeof(response), 0,
383506
(struct sockaddr *)&from_addr, &from_len);
384507

385-
close(sock);
386-
sock = -1;
387-
388-
if (len > 0) {
389-
/* Parse DHCP response */
390-
struct in_addr addr;
391-
/* yiaddr is at offset 16-19 in network byte order */
392-
memcpy(&addr.s_addr, &response[16], sizeof(addr.s_addr));
393-
394-
struct in_addr netmask = {.s_addr = INADDR_ANY};
395-
struct in_addr router = {.s_addr = INADDR_ANY};
396-
/* Clamp MTU to passt's limit */
397-
uint16_t mtu = 65520;
398-
399-
FILE *resolv = fopen("/etc/resolv.conf", "w");
400-
if (!resolv) {
401-
perror("Failed to open /etc/resolv.conf");
402-
}
403-
404-
/* Parse DHCP options (start at offset 240 after magic cookie) */
405-
size_t p = 240;
406-
while (p < (size_t)len) {
407-
unsigned char opt = response[p];
408-
409-
if (opt == 0xff) {
410-
/* Option 255: End (of options) */
411-
break;
412-
}
413-
414-
if (opt == 0) { /* Padding */
415-
p++;
416-
continue;
417-
}
418-
419-
unsigned char opt_len = response[p + 1];
420-
p += 2; /* Length doesn't include code and length field itself */
421-
422-
if (p + opt_len > (size_t)len) {
423-
/* Malformed packet, option length exceeds packet boundary */
424-
break;
425-
}
508+
if (len <= 0)
509+
goto done; /* No DHCP response — not an error, VM may be IPv6-only */
426510

427-
if (opt == 1) {
428-
/* Option 1: Subnet Mask */
429-
memcpy(&netmask.s_addr, &response[p], sizeof(netmask.s_addr));
430-
} else if (opt == 3) {
431-
/* Option 3: Router */
432-
memcpy(&router.s_addr, &response[p], sizeof(router.s_addr));
433-
} else if (opt == 6) {
434-
/* Option 6: Domain Name Server */
435-
if (resolv) {
436-
for (int dns_p = p; dns_p + 3 < p + opt_len; dns_p += 4) {
437-
fprintf(resolv, "nameserver %d.%d.%d.%d\n",
438-
response[dns_p], response[dns_p + 1],
439-
response[dns_p + 2], response[dns_p + 3]);
440-
}
441-
}
442-
} else if (opt == 26) {
443-
/* Option 26: Interface MTU */
444-
mtu = (response[p] << 8) | response[p + 1];
445-
446-
/* We don't know yet if IPv6 is available: don't go below 1280 B
447-
*/
448-
if (mtu < 1280)
449-
mtu = 1280;
450-
if (mtu > 65520)
451-
mtu = 65520;
452-
}
511+
unsigned char msg_type = get_dhcp_msg_type(response, len);
453512

454-
p += opt_len;
513+
if (msg_type == DHCP_MSG_ACK) {
514+
/* Rapid Commit — server sent ACK directly */
515+
close(sock);
516+
sock = -1;
517+
if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0)
518+
goto cleanup;
519+
} else if (msg_type == DHCP_MSG_OFFER) {
520+
/*
521+
* DHCPOFFER — complete the 4-way handshake by sending DHCPREQUEST
522+
* and waiting for DHCPACK. Servers without Rapid Commit (e.g.
523+
* gvproxy) require this.
524+
*/
525+
struct in_addr offered_addr;
526+
memcpy(&offered_addr.s_addr, &response[16],
527+
sizeof(offered_addr.s_addr));
528+
529+
/* Build DHCPREQUEST */
530+
memset(request.options, 0, sizeof(request.options));
531+
opt_offset = 0;
532+
533+
/* Option 53: DHCP Message Type = REQUEST (3) */
534+
request.options[opt_offset++] = 53;
535+
request.options[opt_offset++] = 1;
536+
request.options[opt_offset++] = 3;
537+
538+
/* Option 50: Requested IP Address */
539+
request.options[opt_offset++] = 50;
540+
request.options[opt_offset++] = 4;
541+
memcpy(&request.options[opt_offset], &offered_addr.s_addr, 4);
542+
opt_offset += 4;
543+
544+
/* Option 54: Server Identifier (from_addr) */
545+
request.options[opt_offset++] = 54;
546+
request.options[opt_offset++] = 4;
547+
memcpy(&request.options[opt_offset], &from_addr.sin_addr.s_addr, 4);
548+
opt_offset += 4;
549+
550+
/* Option 255: End */
551+
request.options[opt_offset++] = 0xff;
552+
553+
if (sendto(sock, &request, sizeof(request), 0,
554+
(struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
555+
perror("sendto DHCPREQUEST failed");
556+
goto cleanup;
455557
}
456558

457-
if (resolv) {
458-
fclose(resolv);
459-
}
559+
from_len = sizeof(from_addr);
560+
len = recvfrom(sock, response, sizeof(response), 0,
561+
(struct sockaddr *)&from_addr, &from_len);
460562

461-
/* Calculate prefix length from netmask */
462-
unsigned char prefix_len = count_leading_ones(ntohl(netmask.s_addr));
563+
close(sock);
564+
sock = -1;
463565

464-
if (mod_addr4(nl_sock, iface_index, RTM_NEWADDR, addr, prefix_len) != 0) {
465-
printf("couldn't add the address provided by the DHCP server\n");
566+
if (len <= 0) {
567+
printf("no DHCPACK received\n");
466568
goto cleanup;
467569
}
468-
if (mod_route4(nl_sock, iface_index, RTM_NEWROUTE, router) != 0) {
469-
printf(
470-
"couldn't add the default route provided by the DHCP server\n");
570+
571+
if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0)
471572
goto cleanup;
472-
}
473-
set_mtu(nl_sock, iface_index, mtu);
573+
} else {
574+
printf("unexpected DHCP message type %d\n", msg_type);
575+
goto cleanup;
474576
}
475577

578+
done:
476579
ret = 0;
477-
478580
cleanup:
479581
if (sock >= 0) {
480582
close(sock);

0 commit comments

Comments
 (0)