Skip to content

Commit 1ad1d26

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 9f8dba3 commit 1ad1d26

File tree

1 file changed

+183
-83
lines changed

1 file changed

+183
-83
lines changed

init/dhcp.c

Lines changed: 183 additions & 83 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,126 @@ 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", response[dns_p],
341+
response[dns_p + 1], response[dns_p + 2],
342+
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("couldn't add the default route provided by the DHCP server\n");
373+
return -1;
374+
}
375+
set_mtu(nl_sock, iface_index, mtu);
376+
return 0;
377+
}
378+
257379
/* Send DISCOVER with Rapid Commit, process ACK, configure address and route */
258380
int do_dhcp(const char *iface)
259381
{
@@ -376,106 +498,84 @@ int do_dhcp(const char *iface)
376498
goto cleanup;
377499
}
378500

379-
/* Get and process response (DHCPACK) if any */
501+
/* Get response: DHCPACK (Rapid Commit) or DHCPOFFER */
380502
struct sockaddr_in from_addr;
381503
socklen_t from_len = sizeof(from_addr);
382504
ssize_t len = recvfrom(sock, response, sizeof(response), 0,
383505
(struct sockaddr *)&from_addr, &from_len);
384506

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-
}
507+
if (len <= 0)
508+
goto done; /* No DHCP response — not an error, VM may be IPv6-only */
426509

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

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

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

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

464-
if (mod_addr4(nl_sock, iface_index, RTM_NEWADDR, addr, prefix_len) !=
465-
0) {
466-
printf("couldn't add the address provided by the DHCP server\n");
565+
if (len <= 0) {
566+
printf("no DHCPACK received\n");
467567
goto cleanup;
468568
}
469-
if (mod_route4(nl_sock, iface_index, RTM_NEWROUTE, router) != 0) {
470-
printf(
471-
"couldn't add the default route provided by the DHCP server\n");
569+
570+
if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0)
472571
goto cleanup;
473-
}
474-
set_mtu(nl_sock, iface_index, mtu);
572+
} else {
573+
printf("unexpected DHCP message type %d\n", msg_type);
574+
goto cleanup;
475575
}
476576

577+
done:
477578
ret = 0;
478-
479579
cleanup:
480580
if (sock >= 0) {
481581
close(sock);

0 commit comments

Comments
 (0)