/* * vmnet-common.m - network client wrapper for Apple vmnet.framework * * Copyright(c) 2022 Vladislav Yaroshchuk * Copyright(c) 2021 Phillip Tennen * * This work is licensed under the terms of the GNU GPL, version 2 or later. * See the COPYING file in the top-level directory. * */ #include "qemu/osdep.h" #include "qemu/main-loop.h" #include "qemu/log.h" #include "qapi/qapi-types-net.h" #include "vmnet_int.h" #include "clients.h" #include "qemu/error-report.h" #include "qapi/error.h" #include #include static void vmnet_send_completed(NetClientState *nc, ssize_t len); const char *vmnet_status_map_str(vmnet_return_t status) { switch (status) { case VMNET_SUCCESS: return "success"; case VMNET_FAILURE: return "general failure (possibly not enough privileges)"; case VMNET_MEM_FAILURE: return "memory allocation failure"; case VMNET_INVALID_ARGUMENT: return "invalid argument specified"; case VMNET_SETUP_INCOMPLETE: return "interface setup is not complete"; case VMNET_INVALID_ACCESS: return "invalid access, permission denied"; case VMNET_PACKET_TOO_BIG: return "packet size is larger than MTU"; case VMNET_BUFFER_EXHAUSTED: return "buffers exhausted in kernel"; case VMNET_TOO_MANY_PACKETS: return "packet count exceeds limit"; #if defined(MAC_OS_VERSION_11_0) && \ MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_11_0 case VMNET_SHARING_SERVICE_BUSY: return "conflict, sharing service is in use"; #endif default: return "unknown vmnet error"; } } /** * Write packets from QEMU to vmnet interface. * * vmnet.framework supports iov, but writing more than * one iov into vmnet interface fails with * 'VMNET_INVALID_ARGUMENT'. Collecting provided iovs into * one and passing it to vmnet works fine. That's the * reason why receive_iov() left unimplemented. But it still * works with good performance having .receive() only. */ ssize_t vmnet_receive_common(NetClientState *nc, const uint8_t *buf, size_t size) { VmnetState *s = DO_UPCAST(VmnetState, nc, nc); struct vmpktdesc packet; struct iovec iov; int pkt_cnt; vmnet_return_t if_status; if (size > s->max_packet_size) { warn_report("vmnet: packet is too big, %zu > %" PRIu64, packet.vm_pkt_size, s->max_packet_size); return -1; } iov.iov_base = (char *) buf; iov.iov_len = size; packet.vm_pkt_iovcnt = 1; packet.vm_flags = 0; packet.vm_pkt_size = size; packet.vm_pkt_iov = &iov; pkt_cnt = 1; if_status = vmnet_write(s->vmnet_if, &packet, &pkt_cnt); if (if_status != VMNET_SUCCESS) { error_report("vmnet: write error: %s\n", vmnet_status_map_str(if_status)); return -1; } if (pkt_cnt) { return size; } return 0; } /** * Read packets from vmnet interface and write them * to temporary buffers in VmnetState. * * Returns read packets number (may be 0) on success, * -1 on error */ static int vmnet_read_packets(VmnetState *s) { assert(s->packets_send_current_pos == s->packets_send_end_pos); struct vmpktdesc *packets = s->packets_buf; vmnet_return_t status; int i; /* Read as many packets as present */ s->packets_send_current_pos = 0; s->packets_send_end_pos = VMNET_PACKETS_LIMIT; for (i = 0; i < s->packets_send_end_pos; ++i) { packets[i].vm_pkt_size = s->max_packet_size; packets[i].vm_pkt_iovcnt = 1; packets[i].vm_flags = 0; } status = vmnet_read(s->vmnet_if, packets, &s->packets_send_end_pos); if (status != VMNET_SUCCESS) { error_printf("vmnet: read failed: %s\n", vmnet_status_map_str(status)); s->packets_send_current_pos = 0; s->packets_send_end_pos = 0; return -1; } return s->packets_send_end_pos; } /** * Write packets from temporary buffers in VmnetState * to QEMU. */ static void vmnet_write_packets_to_qemu(VmnetState *s) { while (s->packets_send_current_pos < s->packets_send_end_pos) { ssize_t size = qemu_send_packet_async(&s->nc, s->iov_buf[s->packets_send_current_pos].iov_base, s->packets_buf[s->packets_send_current_pos].vm_pkt_size, vmnet_send_completed); if (size == 0) { /* QEMU is not ready to consume more packets - * stop and wait for completion callback call */ return; } ++s->packets_send_current_pos; } } /** * Bottom half callback that transfers packets from vmnet interface * to QEMU. * * The process of transferring packets is three-staged: * 1. Handle vmnet event; * 2. Read packets from vmnet interface into temporary buffer; * 3. Write packets from temporary buffer to QEMU. * * QEMU may suspend this process on the last stage, returning 0 from * qemu_send_packet_async function. If this happens, we should * respectfully wait until it is ready to consume more packets, * write left ones in temporary buffer and only after this * continue reading more packets from vmnet interface. * * Packets to be transferred are stored into packets_buf, * in the window [packets_send_current_pos..packets_send_end_pos) * including current_pos, excluding end_pos. * * Thus, if QEMU is not ready, buffer is not read and * packets_send_current_pos < packets_send_end_pos. */ static void vmnet_send_bh(void *opaque) { NetClientState *nc = (NetClientState *) opaque; VmnetState *s = DO_UPCAST(VmnetState, nc, nc); /* * Do nothing if QEMU is not ready - wait * for completion callback invocation */ if (s->packets_send_current_pos < s->packets_send_end_pos) { return; } /* Read packets from vmnet interface */ if (vmnet_read_packets(s) > 0) { /* Send them to QEMU */ vmnet_write_packets_to_qemu(s); } } /** * Completion callback to be invoked by QEMU when it becomes * ready to consume more packets. */ static void vmnet_send_completed(NetClientState *nc, ssize_t len) { VmnetState *s = DO_UPCAST(VmnetState, nc, nc); /* Callback is invoked eq queued packet is sent */ ++s->packets_send_current_pos; /* Complete sending packets left in VmnetState buffers */ vmnet_write_packets_to_qemu(s); /* And read new ones from vmnet if VmnetState buffer is ready */ if (s->packets_send_current_pos < s->packets_send_end_pos) { qemu_bh_schedule(s->send_bh); } } static void vmnet_bufs_init(VmnetState *s) { struct vmpktdesc *packets = s->packets_buf; struct iovec *iov = s->iov_buf; int i; for (i = 0; i < VMNET_PACKETS_LIMIT; ++i) { iov[i].iov_len = s->max_packet_size; iov[i].iov_base = g_malloc0(iov[i].iov_len); packets[i].vm_pkt_iov = iov + i; } } int vmnet_if_create(NetClientState *nc, xpc_object_t if_desc, Error **errp) { VmnetState *s = DO_UPCAST(VmnetState, nc, nc); dispatch_semaphore_t if_created_sem = dispatch_semaphore_create(0); __block vmnet_return_t if_status; s->if_queue = dispatch_queue_create( "org.qemu.vmnet.if_queue", DISPATCH_QUEUE_SERIAL ); xpc_dictionary_set_bool( if_desc, vmnet_allocate_mac_address_key, false ); #ifdef DEBUG qemu_log("vmnet.start.interface_desc:\n"); xpc_dictionary_apply(if_desc, ^bool(const char *k, xpc_object_t v) { char *desc = xpc_copy_description(v); qemu_log(" %s=%s\n", k, desc); free(desc); return true; }); #endif /* DEBUG */ s->vmnet_if = vmnet_start_interface( if_desc, s->if_queue, ^(vmnet_return_t status, xpc_object_t interface_param) { if_status = status; if (status != VMNET_SUCCESS || !interface_param) { dispatch_semaphore_signal(if_created_sem); return; } #ifdef DEBUG qemu_log("vmnet.start.interface_param:\n"); xpc_dictionary_apply(interface_param, ^bool(const char *k, xpc_object_t v) { char *desc = xpc_copy_description(v); qemu_log(" %s=%s\n", k, desc); free(desc); return true; }); #endif /* DEBUG */ s->mtu = xpc_dictionary_get_uint64( interface_param, vmnet_mtu_key); s->max_packet_size = xpc_dictionary_get_uint64( interface_param, vmnet_max_packet_size_key); dispatch_semaphore_signal(if_created_sem); }); if (s->vmnet_if == NULL) { dispatch_release(s->if_queue); dispatch_release(if_created_sem); error_setg(errp, "unable to create interface with requested params"); return -1; } dispatch_semaphore_wait(if_created_sem, DISPATCH_TIME_FOREVER); dispatch_release(if_created_sem); if (if_status != VMNET_SUCCESS) { dispatch_release(s->if_queue); error_setg(errp, "cannot create vmnet interface: %s", vmnet_status_map_str(if_status)); return -1; } s->send_bh = aio_bh_new(qemu_get_aio_context(), vmnet_send_bh, nc); vmnet_bufs_init(s); s->packets_send_current_pos = 0; s->packets_send_end_pos = 0; vmnet_interface_set_event_callback( s->vmnet_if, VMNET_INTERFACE_PACKETS_AVAILABLE, s->if_queue, ^(interface_event_t event_id, xpc_object_t event) { assert(event_id == VMNET_INTERFACE_PACKETS_AVAILABLE); /* * This function is being called from a non qemu thread, so * we only schedule a BH, and do the rest of the io completion * handling from vmnet_send_bh() which runs in a qemu context. */ qemu_bh_schedule(s->send_bh); }); return 0; } void vmnet_cleanup_common(NetClientState *nc) { VmnetState *s = DO_UPCAST(VmnetState, nc, nc); dispatch_semaphore_t if_stopped_sem; if (s->vmnet_if == NULL) { return; } if_stopped_sem = dispatch_semaphore_create(0); vmnet_stop_interface( s->vmnet_if, s->if_queue, ^(vmnet_return_t status) { assert(status == VMNET_SUCCESS); dispatch_semaphore_signal(if_stopped_sem); }); dispatch_semaphore_wait(if_stopped_sem, DISPATCH_TIME_FOREVER); qemu_purge_queued_packets(nc); qemu_bh_delete(s->send_bh); dispatch_release(if_stopped_sem); dispatch_release(s->if_queue); for (int i = 0; i < VMNET_PACKETS_LIMIT; ++i) { g_free(s->iov_buf[i].iov_base); } }