Skip to content

Commit 8ca37c4

Browse files
committed
feat: add --vmnet-disable-dhcp to disable the vmnet DHCP server (macOS 26+)
Opt-in flag that starts the interface via the macOS 26 vmnet_network_configuration API with the DHCP server disabled, so an external DHCP server can own the subnet while shared-mode NAT is preserved. The default vmnet_start_interface path is unchanged; the new path is gated at compile time (SDK) and at runtime (__builtin_available) and errors cleanly on older macOS. Adds an integration test that boots a guest through the flag and asserts it gets no DHCP lease (self-skips below macOS 26; CI matrix already includes macos-26). Signed-off-by: fpolica91 <fabriciopolicarpo0@gmail.com>
1 parent 8701ef4 commit 8ca37c4

6 files changed

Lines changed: 157 additions & 32 deletions

File tree

.github/workflows/qemu.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ jobs:
4646
brew install qemu bash
4747
- name: Test (shared mode)
4848
run: ./test/test.sh /var/run/socket_vmnet
49+
- name: Test (disable DHCP, macOS 26+)
50+
run: ./test/test-disable-dhcp.sh
4951
# Bridged mode cannot be tested on GHA

cli.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ static void print_usage(const char *argv0) {
6363
printf(" The prefix must be a ULA i.e. "
6464
"start with fd00::/8.\n");
6565
printf(" (default: random)\n");
66+
printf("--vmnet-disable-dhcp disable the vmnet DHCP server "
67+
"(requires macOS 26;\n");
68+
printf(" lets an external DHCP server own "
69+
"the subnet)\n");
6670
printf("-p, --pidfile=PIDFILE save pid to PIDFILE\n");
6771
printf("-h, --help display this help and exit\n");
6872
printf("-v, --version display version information and "
@@ -83,6 +87,7 @@ enum {
8387
CLI_OPT_VMNET_INTERFACE_ID,
8488
CLI_OPT_VMNET_NAT66_PREFIX,
8589
CLI_OPT_VMNET_NETWORK_IDENTIFIER,
90+
CLI_OPT_VMNET_DISABLE_DHCP,
8691
};
8792

8893
struct cli_options *cli_options_parse(int argc, char *argv[]) {
@@ -102,6 +107,7 @@ struct cli_options *cli_options_parse(int argc, char *argv[]) {
102107
{"vmnet-interface-id", required_argument, NULL, CLI_OPT_VMNET_INTERFACE_ID },
103108
{"vmnet-nat66-prefix", required_argument, NULL, CLI_OPT_VMNET_NAT66_PREFIX },
104109
{"vmnet-network-identifier", required_argument, NULL, CLI_OPT_VMNET_NETWORK_IDENTIFIER},
110+
{"vmnet-disable-dhcp", no_argument, NULL, CLI_OPT_VMNET_DISABLE_DHCP },
105111
{"pidfile", required_argument, NULL, 'p' },
106112
{"help", no_argument, NULL, 'h' },
107113
{"version", no_argument, NULL, 'v' },
@@ -152,6 +158,9 @@ struct cli_options *cli_options_parse(int argc, char *argv[]) {
152158
goto error;
153159
}
154160
break;
161+
case CLI_OPT_VMNET_DISABLE_DHCP:
162+
res->vmnet_disable_dhcp = true;
163+
break;
155164
case 'p':
156165
res->pidfile = strdup(optarg);
157166
break;

cli.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ struct cli_options {
2424
uuid_t vmnet_network_identifier;
2525
// --vmnet-nat66-prefix, corresponds to vmnet_nat66_prefix_key
2626
char *vmnet_nat66_prefix;
27+
// --vmnet-disable-dhcp; disables the vmnet DHCP server (requires macOS 26)
28+
bool vmnet_disable_dhcp;
2729
// -p, --pidfile; writes pidfile using permissions of socket_vmnet
2830
char *pidfile;
2931
// arg

main.c

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -224,48 +224,100 @@ static void on_vmnet_packets_available(interface_ref iface, int64_t estim_count,
224224

225225
static interface_ref start(struct state *state, struct cli_options *cliopt) {
226226
INFOF("Initializing vmnet.framework (mode %d)", cliopt->vmnet_mode);
227-
xpc_object_t dict = xpc_dictionary_create(NULL, NULL, 0);
228-
xpc_dictionary_set_uint64(dict, vmnet_operation_mode_key, cliopt->vmnet_mode);
229-
if (cliopt->vmnet_interface != NULL) {
230-
INFOF("Using network interface \"%s\"", cliopt->vmnet_interface);
231-
xpc_dictionary_set_string(dict, vmnet_shared_interface_name_key, cliopt->vmnet_interface);
232-
}
233-
234-
if (!uuid_is_null(cliopt->vmnet_network_identifier)) {
235-
xpc_dictionary_set_uuid(dict, vmnet_network_identifier_key, cliopt->vmnet_network_identifier);
236-
}
237-
238-
if (cliopt->vmnet_gateway != NULL) {
239-
xpc_dictionary_set_string(dict, vmnet_start_address_key, cliopt->vmnet_gateway);
240-
xpc_dictionary_set_string(dict, vmnet_end_address_key, cliopt->vmnet_dhcp_end);
241-
xpc_dictionary_set_string(dict, vmnet_subnet_mask_key, cliopt->vmnet_mask);
242-
}
243-
244-
xpc_dictionary_set_uuid(dict, vmnet_interface_id_key, cliopt->vmnet_interface_id);
245-
246-
if (cliopt->vmnet_nat66_prefix != NULL) {
247-
xpc_dictionary_set_string(dict, vmnet_nat66_prefix_key, cliopt->vmnet_nat66_prefix);
248-
}
249227

250228
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
251-
252-
__block interface_ref iface;
253-
__block vmnet_return_t status;
254-
229+
__block interface_ref iface = NULL;
230+
__block vmnet_return_t status = VMNET_FAILURE;
255231
__block uint64_t max_bytes = 0;
256-
iface = vmnet_start_interface(
257-
dict, state->host_queue, ^(vmnet_return_t x_status, xpc_object_t x_param) {
232+
vmnet_start_interface_completion_handler_t on_started =
233+
^(vmnet_return_t x_status, xpc_object_t x_param) {
258234
status = x_status;
259235
if (x_status == VMNET_SUCCESS) {
260236
print_vmnet_start_param(x_param);
261237
max_bytes = xpc_dictionary_get_uint64(x_param, vmnet_max_packet_size_key);
262238
}
263239
dispatch_semaphore_signal(sem);
264-
});
265-
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
266-
xpc_release(dict);
240+
};
241+
242+
if (cliopt->vmnet_disable_dhcp) {
243+
// The DHCP server can only be disabled via the vmnet_network_configuration
244+
// API, which is macOS 26+. Guard at both compile time (SDK has the symbols)
245+
// and runtime (the host actually provides them).
246+
#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000
247+
if (__builtin_available(macOS 26.0, *)) {
248+
vmnet_return_t st = VMNET_FAILURE;
249+
vmnet_network_configuration_ref cfg =
250+
vmnet_network_configuration_create(cliopt->vmnet_mode, &st);
251+
if (cfg == NULL) {
252+
ERRORF("vmnet_network_configuration_create: [%d] %s", st, vmnet_strerror(st));
253+
return NULL;
254+
}
255+
if (cliopt->vmnet_interface != NULL) {
256+
INFOF("Using network interface \"%s\"", cliopt->vmnet_interface);
257+
st = vmnet_network_configuration_set_external_interface(cfg, cliopt->vmnet_interface);
258+
if (st != VMNET_SUCCESS) {
259+
ERRORF("vmnet_network_configuration_set_external_interface: [%d] %s", st,
260+
vmnet_strerror(st));
261+
return NULL;
262+
}
263+
}
264+
if (cliopt->vmnet_gateway != NULL) {
265+
struct in_addr subnet, mask;
266+
if (!inet_aton(cliopt->vmnet_gateway, &subnet) || !inet_aton(cliopt->vmnet_mask, &mask)) {
267+
ERRORN("inet_aton");
268+
return NULL;
269+
}
270+
vmnet_network_configuration_set_ipv4_subnet(cfg, &subnet, &mask);
271+
}
272+
vmnet_network_configuration_disable_dhcp(cfg);
273+
vmnet_network_ref net = vmnet_network_create(cfg, &st);
274+
if (net == NULL) {
275+
ERRORF("vmnet_network_create: [%d] %s", st, vmnet_strerror(st));
276+
return NULL;
277+
}
278+
xpc_object_t desc = xpc_dictionary_create(NULL, NULL, 0);
279+
iface = vmnet_interface_start_with_network(net, desc, state->host_queue, on_started);
280+
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
281+
xpc_release(desc);
282+
} else {
283+
ERROR("--vmnet-disable-dhcp requires macOS 26.0 or later");
284+
return NULL;
285+
}
286+
#else
287+
ERROR("--vmnet-disable-dhcp requires building against the macOS 26 SDK or later");
288+
return NULL;
289+
#endif
290+
} else {
291+
xpc_object_t dict = xpc_dictionary_create(NULL, NULL, 0);
292+
xpc_dictionary_set_uint64(dict, vmnet_operation_mode_key, cliopt->vmnet_mode);
293+
if (cliopt->vmnet_interface != NULL) {
294+
INFOF("Using network interface \"%s\"", cliopt->vmnet_interface);
295+
xpc_dictionary_set_string(dict, vmnet_shared_interface_name_key, cliopt->vmnet_interface);
296+
}
297+
298+
if (!uuid_is_null(cliopt->vmnet_network_identifier)) {
299+
xpc_dictionary_set_uuid(dict, vmnet_network_identifier_key, cliopt->vmnet_network_identifier);
300+
}
301+
302+
if (cliopt->vmnet_gateway != NULL) {
303+
xpc_dictionary_set_string(dict, vmnet_start_address_key, cliopt->vmnet_gateway);
304+
xpc_dictionary_set_string(dict, vmnet_end_address_key, cliopt->vmnet_dhcp_end);
305+
xpc_dictionary_set_string(dict, vmnet_subnet_mask_key, cliopt->vmnet_mask);
306+
}
307+
308+
xpc_dictionary_set_uuid(dict, vmnet_interface_id_key, cliopt->vmnet_interface_id);
309+
310+
if (cliopt->vmnet_nat66_prefix != NULL) {
311+
xpc_dictionary_set_string(dict, vmnet_nat66_prefix_key, cliopt->vmnet_nat66_prefix);
312+
}
313+
314+
iface = vmnet_start_interface(dict, state->host_queue, on_started);
315+
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
316+
xpc_release(dict);
317+
}
318+
267319
if (status != VMNET_SUCCESS) {
268-
ERRORF("vmnet_start_interface: [%d] %s", status, vmnet_strerror(status));
320+
ERRORF("vmnet start interface: [%d] %s", status, vmnet_strerror(status));
269321
return NULL;
270322
}
271323

test/test-disable-dhcp.ipxe

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!ipxe
2+
echo <socket_vmnet:disable-dhcp-testing>
3+
dhcp || echo dhcp-failed-as-expected
4+
echo net0-ip=${net0/ip}
5+
echo </socket_vmnet:disable-dhcp-testing>
6+
sleep 1
7+
reboot

test/test-disable-dhcp.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/bash
2+
set -eux -o pipefail
3+
cd "$(dirname "$0")"
4+
5+
os_major="$(sw_vers -productVersion | cut -d. -f1)"
6+
if [ "$os_major" -lt 26 ]; then
7+
echo >&2 "SKIP: --vmnet-disable-dhcp requires macOS 26 (host is $(sw_vers -productVersion))"
8+
exit 0
9+
fi
10+
11+
SOCKET=/var/run/socket_vmnet.disable-dhcp
12+
GATEWAY=192.168.106.1
13+
14+
if [ ! -f ipxe.lkrn ]; then
15+
curl -fSL -O https://boot.ipxe.org/ipxe.lkrn
16+
fi
17+
18+
sudo /opt/socket_vmnet/bin/socket_vmnet --vmnet-disable-dhcp \
19+
--vmnet-gateway="${GATEWAY}" --socket-group=staff "${SOCKET}" &
20+
socket_vmnet_pid=$!
21+
cleanup() {
22+
sudo kill "${socket_vmnet_pid}" 2>/dev/null || true
23+
sudo rm -f "${SOCKET}"
24+
}
25+
trap cleanup EXIT
26+
27+
for _ in $(seq 1 30); do
28+
[ -S "${SOCKET}" ] && break
29+
sleep 1
30+
done
31+
if [ ! -S "${SOCKET}" ]; then
32+
echo >&2 "ERROR: socket_vmnet did not create ${SOCKET}"
33+
exit 1
34+
fi
35+
36+
rm -f serial.log
37+
echo >&2 "===== QEMU BEGIN ====="
38+
/opt/socket_vmnet/bin/socket_vmnet_client "${SOCKET}" qemu-system-x86_64 \
39+
-device virtio-net-pci,netdev=net0 \
40+
-netdev socket,id=net0,fd=3 \
41+
-kernel ipxe.lkrn \
42+
-initrd test-disable-dhcp.ipxe \
43+
-no-reboot \
44+
-nographic 2>&1 | tee serial.log
45+
echo >&2 "===== QEMU FINISH ====="
46+
47+
if grep -q "net0-ip=${GATEWAY%.*}." serial.log; then
48+
echo >&2 "ERROR: guest obtained a DHCP lease, but DHCP should be disabled"
49+
exit 1
50+
fi
51+
52+
echo >&2 "OK: guest obtained no DHCP lease (DHCP disabled as expected)"
53+
rm -f serial.log

0 commit comments

Comments
 (0)