Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/qemu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ jobs:
brew install qemu bash
- name: Test (shared mode)
run: ./test/test.sh /var/run/socket_vmnet
- name: Test (disable DHCP, macOS 26+)
run: ./test/test-disable-dhcp.sh
# Bridged mode cannot be tested on GHA
22 changes: 22 additions & 0 deletions cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ static void print_usage(const char *argv0) {
printf(" The prefix must be a ULA i.e. "
"start with fd00::/8.\n");
printf(" (default: random)\n");
printf("--vmnet-disable-dhcp disable the vmnet DHCP server "
"(requires macOS 26;\n");
printf(" lets an external DHCP server own "
"the subnet)\n");
printf("-p, --pidfile=PIDFILE save pid to PIDFILE\n");
printf("-h, --help display this help and exit\n");
printf("-v, --version display version information and "
Expand All @@ -83,6 +87,7 @@ enum {
CLI_OPT_VMNET_INTERFACE_ID,
CLI_OPT_VMNET_NAT66_PREFIX,
CLI_OPT_VMNET_NETWORK_IDENTIFIER,
CLI_OPT_VMNET_DISABLE_DHCP,
};

struct cli_options *cli_options_parse(int argc, char *argv[]) {
Expand All @@ -102,6 +107,7 @@ struct cli_options *cli_options_parse(int argc, char *argv[]) {
{"vmnet-interface-id", required_argument, NULL, CLI_OPT_VMNET_INTERFACE_ID },
{"vmnet-nat66-prefix", required_argument, NULL, CLI_OPT_VMNET_NAT66_PREFIX },
{"vmnet-network-identifier", required_argument, NULL, CLI_OPT_VMNET_NETWORK_IDENTIFIER},
{"vmnet-disable-dhcp", no_argument, NULL, CLI_OPT_VMNET_DISABLE_DHCP },
{"pidfile", required_argument, NULL, 'p' },
{"help", no_argument, NULL, 'h' },
{"version", no_argument, NULL, 'v' },
Expand Down Expand Up @@ -152,6 +158,9 @@ struct cli_options *cli_options_parse(int argc, char *argv[]) {
goto error;
}
break;
case CLI_OPT_VMNET_DISABLE_DHCP:
res->vmnet_disable_dhcp = true;
break;
case 'p':
res->pidfile = strdup(optarg);
break;
Expand All @@ -173,6 +182,19 @@ struct cli_options *cli_options_parse(int argc, char *argv[]) {
}
res->socket_path = strdup(argv[optind]);

/* warn before the defaults below are filled in, so that only explicitly
* specified values match */
if (res->vmnet_disable_dhcp) {
if (res->vmnet_dhcp_end != NULL)
WARN("--vmnet-dhcp-end is ignored with --vmnet-disable-dhcp: no DHCP server is started");
if (!uuid_is_null(res->vmnet_interface_id))
WARN("--vmnet-interface-id is ignored with --vmnet-disable-dhcp: "
"vmnet_interface_start_with_network assigns its own interface id");
if (!uuid_is_null(res->vmnet_network_identifier))
WARN("--vmnet-network-identifier is ignored with --vmnet-disable-dhcp: "
"the macOS 26 vmnet network configuration API has no equivalent");
}

/* fill default */
if (res->socket_group == NULL)
res->socket_group = strdup(CLI_DEFAULT_SOCKET_GROUP); /* use strdup to make it freeable */
Expand Down
2 changes: 2 additions & 0 deletions cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct cli_options {
uuid_t vmnet_network_identifier;
// --vmnet-nat66-prefix, corresponds to vmnet_nat66_prefix_key
char *vmnet_nat66_prefix;
// --vmnet-disable-dhcp; disables the vmnet DHCP server (requires macOS 26)
bool vmnet_disable_dhcp;
// -p, --pidfile; writes pidfile using permissions of socket_vmnet
char *pidfile;
// arg
Expand Down
136 changes: 104 additions & 32 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -224,48 +224,120 @@ static void on_vmnet_packets_available(interface_ref iface, int64_t estim_count,

static interface_ref start(struct state *state, struct cli_options *cliopt) {
INFOF("Initializing vmnet.framework (mode %d)", cliopt->vmnet_mode);
xpc_object_t dict = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_uint64(dict, vmnet_operation_mode_key, cliopt->vmnet_mode);
if (cliopt->vmnet_interface != NULL) {
INFOF("Using network interface \"%s\"", cliopt->vmnet_interface);
xpc_dictionary_set_string(dict, vmnet_shared_interface_name_key, cliopt->vmnet_interface);
}

if (!uuid_is_null(cliopt->vmnet_network_identifier)) {
xpc_dictionary_set_uuid(dict, vmnet_network_identifier_key, cliopt->vmnet_network_identifier);
}

if (cliopt->vmnet_gateway != NULL) {
xpc_dictionary_set_string(dict, vmnet_start_address_key, cliopt->vmnet_gateway);
xpc_dictionary_set_string(dict, vmnet_end_address_key, cliopt->vmnet_dhcp_end);
xpc_dictionary_set_string(dict, vmnet_subnet_mask_key, cliopt->vmnet_mask);
}

xpc_dictionary_set_uuid(dict, vmnet_interface_id_key, cliopt->vmnet_interface_id);

if (cliopt->vmnet_nat66_prefix != NULL) {
xpc_dictionary_set_string(dict, vmnet_nat66_prefix_key, cliopt->vmnet_nat66_prefix);
}

dispatch_semaphore_t sem = dispatch_semaphore_create(0);

__block interface_ref iface;
__block vmnet_return_t status;

__block interface_ref iface = NULL;
__block vmnet_return_t status = VMNET_FAILURE;
__block uint64_t max_bytes = 0;
iface = vmnet_start_interface(
dict, state->host_queue, ^(vmnet_return_t x_status, xpc_object_t x_param) {
vmnet_start_interface_completion_handler_t on_started =
^(vmnet_return_t x_status, xpc_object_t x_param) {
status = x_status;
if (x_status == VMNET_SUCCESS) {
print_vmnet_start_param(x_param);
max_bytes = xpc_dictionary_get_uint64(x_param, vmnet_max_packet_size_key);
}
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
xpc_release(dict);
};

if (cliopt->vmnet_disable_dhcp) {
// The DHCP server can only be disabled via the vmnet_network_configuration
// API, which is macOS 26+. Guard at both compile time (SDK has the symbols)
// and runtime (the host actually provides them).
#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000
if (__builtin_available(macOS 26.0, *)) {
vmnet_return_t st = VMNET_FAILURE;
vmnet_network_configuration_ref cfg =
vmnet_network_configuration_create(cliopt->vmnet_mode, &st);
if (cfg == NULL) {
ERRORF("vmnet_network_configuration_create: [%d] %s", st, vmnet_strerror(st));
return NULL;
}
if (cliopt->vmnet_interface != NULL) {
INFOF("Using network interface \"%s\"", cliopt->vmnet_interface);
st = vmnet_network_configuration_set_external_interface(cfg, cliopt->vmnet_interface);
if (st != VMNET_SUCCESS) {
ERRORF("vmnet_network_configuration_set_external_interface: [%d] %s", st,
vmnet_strerror(st));
return NULL;
}
}
if (cliopt->vmnet_gateway != NULL) {
struct in_addr gateway, subnet, mask;
if (!inet_aton(cliopt->vmnet_gateway, &gateway)) {
ERRORF("invalid address \"%s\" was specified for --vmnet-gateway", cliopt->vmnet_gateway);
return NULL;
}
if (!inet_aton(cliopt->vmnet_mask, &mask)) {
ERRORF("invalid address \"%s\" was specified for --vmnet-mask", cliopt->vmnet_mask);
return NULL;
}
subnet = gateway;
subnet.s_addr &= mask.s_addr;
vmnet_network_configuration_set_ipv4_subnet(cfg, &subnet, &mask);
}
if (cliopt->vmnet_nat66_prefix != NULL) {
struct in6_addr prefix;
if (inet_pton(AF_INET6, cliopt->vmnet_nat66_prefix, &prefix) != 1) {
ERRORF("invalid IPv6 prefix \"%s\" for --vmnet-nat66-prefix", cliopt->vmnet_nat66_prefix);
return NULL;
}
st = vmnet_network_configuration_set_ipv6_prefix(cfg, &prefix, 64);
if (st != VMNET_SUCCESS) {
ERRORF("vmnet_network_configuration_set_ipv6_prefix: [%d] %s", st, vmnet_strerror(st));
return NULL;
}
}
vmnet_network_configuration_disable_dhcp(cfg);
vmnet_network_ref net = vmnet_network_create(cfg, &st);
if (net == NULL) {
ERRORF("vmnet_network_create: [%d] %s", st, vmnet_strerror(st));
return NULL;
}
xpc_object_t desc = xpc_dictionary_create(NULL, NULL, 0);
iface = vmnet_interface_start_with_network(net, desc, state->host_queue, on_started);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
xpc_release(desc);
Comment thread
fpolica91 marked this conversation as resolved.
} else {
ERROR("--vmnet-disable-dhcp requires macOS 26.0 or later");
return NULL;
}
#else
ERROR("--vmnet-disable-dhcp requires building against the macOS 26 SDK or later");
return NULL;
#endif
} else {
xpc_object_t dict = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_uint64(dict, vmnet_operation_mode_key, cliopt->vmnet_mode);
if (cliopt->vmnet_interface != NULL) {
INFOF("Using network interface \"%s\"", cliopt->vmnet_interface);
xpc_dictionary_set_string(dict, vmnet_shared_interface_name_key, cliopt->vmnet_interface);
}

if (!uuid_is_null(cliopt->vmnet_network_identifier)) {
xpc_dictionary_set_uuid(dict, vmnet_network_identifier_key, cliopt->vmnet_network_identifier);
}

if (cliopt->vmnet_gateway != NULL) {
xpc_dictionary_set_string(dict, vmnet_start_address_key, cliopt->vmnet_gateway);
xpc_dictionary_set_string(dict, vmnet_end_address_key, cliopt->vmnet_dhcp_end);
xpc_dictionary_set_string(dict, vmnet_subnet_mask_key, cliopt->vmnet_mask);
}

xpc_dictionary_set_uuid(dict, vmnet_interface_id_key, cliopt->vmnet_interface_id);

if (cliopt->vmnet_nat66_prefix != NULL) {
xpc_dictionary_set_string(dict, vmnet_nat66_prefix_key, cliopt->vmnet_nat66_prefix);
}

iface = vmnet_start_interface(dict, state->host_queue, on_started);
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
xpc_release(dict);
}

if (status != VMNET_SUCCESS) {
ERRORF("vmnet_start_interface: [%d] %s", status, vmnet_strerror(status));
const char *start_api =
cliopt->vmnet_disable_dhcp ? "vmnet_interface_start_with_network" : "vmnet_start_interface";
ERRORF("%s: [%d] %s", start_api, status, vmnet_strerror(status));
return NULL;
}

Expand Down
7 changes: 7 additions & 0 deletions test/test-disable-dhcp.ipxe
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!ipxe
echo <socket_vmnet:disable-dhcp-testing>
dhcp || echo dhcp-failed-as-expected
echo net0-ip=${net0/ip}
echo </socket_vmnet:disable-dhcp-testing>
sleep 1
reboot
62 changes: 62 additions & 0 deletions test/test-disable-dhcp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/bash
set -eux -o pipefail
cd "$(dirname "$0")"

os_major="$(sw_vers -productVersion | cut -d. -f1)"
if [ "$os_major" -lt 26 ]; then
echo >&2 "SKIP: --vmnet-disable-dhcp requires macOS 26 (host is $(sw_vers -productVersion))"
exit 0
fi

SOCKET=/var/run/socket_vmnet.disable-dhcp
GATEWAY=192.168.106.1

if [ ! -f ipxe.lkrn ]; then
curl -fSL -O https://boot.ipxe.org/ipxe.lkrn
fi

PIDFILE="$(mktemp -t socket_vmnet.pid.XXXXXX)"

sudo /opt/socket_vmnet/bin/socket_vmnet --pidfile "${PIDFILE}" --vmnet-disable-dhcp \
--vmnet-gateway="${GATEWAY}" --socket-group=staff "${SOCKET}" &
cleanup() {
if [ -f "${PIDFILE}" ]; then
sudo kill "$(cat "${PIDFILE}")" 2>/dev/null || true
rm -f "${PIDFILE}"
fi
sudo rm -f "${SOCKET}"
}
trap cleanup EXIT

for _ in $(seq 1 30); do
[ -S "${SOCKET}" ] && break
sleep 1
done
if [ ! -S "${SOCKET}" ]; then
echo >&2 "ERROR: socket_vmnet did not create ${SOCKET}"
exit 1
fi

rm -f serial.log
echo >&2 "===== QEMU BEGIN ====="
/opt/socket_vmnet/bin/socket_vmnet_client "${SOCKET}" qemu-system-x86_64 \
-device virtio-net-pci,netdev=net0 \
-netdev socket,id=net0,fd=3 \
-kernel ipxe.lkrn \
-initrd test-disable-dhcp.ipxe \
-no-reboot \
-nographic 2>&1 | tee serial.log
echo >&2 "===== QEMU FINISH ====="

if ! grep -q "dhcp-failed-as-expected" serial.log; then
echo >&2 "ERROR: guest obtained a DHCP lease, but DHCP should be disabled"
exit 1
fi

if grep -Eq "net0-ip=[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" serial.log; then
echo >&2 "ERROR: guest has an IPv4 address despite DHCP being disabled"
exit 1
fi

echo >&2 "OK: guest obtained no DHCP lease (DHCP disabled as expected)"
rm -f serial.log
Loading