diff --git a/meson.build b/meson.build index 155421986..8fab6160a 100644 --- a/meson.build +++ b/meson.build @@ -25,6 +25,7 @@ opt_static = get_option('static') opt_systemd = get_option('systemd') opt_tests = get_option('tests') opt_tunemu = get_option('tunemu') +opt_vmnet = get_option('vmnet') opt_uml = get_option('uml') opt_vde = get_option('vde') opt_zlib = get_option('zlib') diff --git a/meson_options.txt b/meson_options.txt index 0df57950c..412ee5430 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -79,6 +79,11 @@ option('tunemu', value: 'auto', description: 'support for the tunemu driver') +option('vmnet', + type: 'feature', + value: 'auto', + description: 'support for the vmnet driver') + option('vde', type: 'feature', value: 'auto', diff --git a/src/bsd/darwin/meson.build b/src/bsd/darwin/meson.build index f7dc99e25..3b5e4b9e0 100644 --- a/src/bsd/darwin/meson.build +++ b/src/bsd/darwin/meson.build @@ -1,5 +1,6 @@ dep_tunemu = dependency('tunemu', required: opt_tunemu, static: static) dep_pcap = dependency('pcap', required: opt_tunemu, static: static) +dep_vmnet = dependency('vmnet', required: opt_vmnet, static: static) if dep_tunemu.found() and dep_pcap.found() deps_tincd += [dep_tunemu, dep_pcap] @@ -7,6 +8,11 @@ if dep_tunemu.found() and dep_pcap.found() cdata.set('ENABLE_TUNEMU', 1) endif +if dep_vmnet.found() + deps_tincd += [dep_vmnet] + src_tincd += files('vmnet.c') + cdata.set('ENABLE_VMNET', 1) +endif + # macOS apparently doesn't support kqueue with TAP devices src_tincd += src_event_select - diff --git a/src/bsd/darwin/vmnet.c b/src/bsd/darwin/vmnet.c new file mode 100644 index 000000000..5eceba201 --- /dev/null +++ b/src/bsd/darwin/vmnet.c @@ -0,0 +1,204 @@ +/* + * vmnet - Tun device emulation for Darwin + * Copyright (C) 2024 Eric Karge + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "vmnet.h" +#include +#include +#include +#include +#include +#include +#include "../../logger.h" +#include + +static volatile vmnet_return_t if_status = VMNET_SETUP_INCOMPLETE; +static dispatch_queue_t if_queue; +static interface_ref vmnet_if; +static size_t max_packet_size; +static struct iovec read_iov_in; +static int read_socket[2]; + +static void macos_vmnet_read(void); +static const char *str_vmnet_status(vmnet_return_t status); + +int macos_vmnet_open(void) { + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, read_socket)) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unable to create socket: %s", strerror(errno)); + return -1; + } + + if_queue = dispatch_queue_create("org.tinc-vpn.vmnet.if_queue", DISPATCH_QUEUE_SERIAL); + + xpc_object_t if_desc = xpc_dictionary_create(NULL, NULL, 0); + xpc_dictionary_set_uint64(if_desc, vmnet_operation_mode_key, VMNET_HOST_MODE); + xpc_dictionary_set_bool(if_desc, vmnet_enable_isolation_key, 0); + xpc_dictionary_set_bool(if_desc, vmnet_allocate_mac_address_key, false); + + dispatch_semaphore_t if_started_sem = dispatch_semaphore_create(0); + vmnet_if = vmnet_start_interface( + if_desc, if_queue, + ^(vmnet_return_t status, xpc_object_t interface_param) { + if_status = status; + if (status == VMNET_SUCCESS && interface_param) { + max_packet_size = xpc_dictionary_get_uint64(interface_param, vmnet_max_packet_size_key); + } + dispatch_semaphore_signal(if_started_sem); + }); + dispatch_semaphore_wait(if_started_sem, DISPATCH_TIME_FOREVER); + dispatch_release(if_started_sem); + + xpc_release(if_desc); + + if(if_status != VMNET_SUCCESS) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unable to create vmnet device: %s", str_vmnet_status(if_status)); + return -1; + } + + read_iov_in.iov_base = malloc(max_packet_size); + read_iov_in.iov_len = max_packet_size; + + vmnet_interface_set_event_callback( + vmnet_if, VMNET_INTERFACE_PACKETS_AVAILABLE, if_queue, + ^(interface_event_t event_type, xpc_object_t event) { + macos_vmnet_read(); + }); + + return read_socket[0]; +} + +int macos_vmnet_close(int fd) { + if (vmnet_if == NULL || fd != read_socket[0]) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unable to close vmnet device: device not setup properly"); + return -1; + } + + vmnet_interface_set_event_callback(vmnet_if, VMNET_INTERFACE_PACKETS_AVAILABLE, NULL, NULL); + + dispatch_semaphore_t if_stopped_sem = dispatch_semaphore_create(0); + vmnet_stop_interface( + vmnet_if, if_queue, + ^(vmnet_return_t status) { + if_status = status; + dispatch_semaphore_signal(if_stopped_sem); + }); + dispatch_semaphore_wait(if_stopped_sem, DISPATCH_TIME_FOREVER); + dispatch_release(if_stopped_sem); + + if (if_status != VMNET_SUCCESS) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unable to close vmnet device: %s", str_vmnet_status(if_status)); + return -1; + } + if_status = VMNET_SETUP_INCOMPLETE; + + dispatch_release(if_queue); + + read_iov_in.iov_len = 0; + free(read_iov_in.iov_base); + read_iov_in.iov_base = NULL; + + close(read_socket[0]); + close(read_socket[1]); + + return 0; +} + +void macos_vmnet_read(void) { + if (if_status != VMNET_SUCCESS) { + return; + } + + int pkt_count = 1; + struct vmpktdesc packet = { + .vm_flags = 0, + .vm_pkt_size = max_packet_size, + .vm_pkt_iov = &read_iov_in, + .vm_pkt_iovcnt = 1, + }; + + if_status = vmnet_read(vmnet_if, &packet, &pkt_count); + if (if_status != VMNET_SUCCESS) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unable to read packet: %s", str_vmnet_status(if_status)); + return; + } + + if ( pkt_count && packet.vm_pkt_iovcnt ) { + struct iovec iov_out = { + .iov_base = packet.vm_pkt_iov->iov_base, + .iov_len = packet.vm_pkt_size, + }; + if(writev(read_socket[1], &iov_out, 1) < 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unable to write to read socket: %s", strerror(errno)); + return; + } + } +} + +ssize_t macos_vmnet_write(uint8_t *buffer, size_t buflen) { + if (buflen > max_packet_size) { + logger(DEBUG_ALWAYS, LOG_ERR, "Max packet size (%zd) exceeded: %zd", max_packet_size, buflen); + return -1; + } + + struct iovec iov = { + .iov_base = (char *) buffer, + .iov_len = buflen, + }; + struct vmpktdesc packet = { + .vm_pkt_iovcnt = 1, + .vm_flags = 0, + .vm_pkt_size = buflen, + .vm_pkt_iov = &iov, + }; + int pkt_count = 1; + + vmnet_return_t result = vmnet_write(vmnet_if, &packet, &pkt_count); + if (result != VMNET_SUCCESS) { + logger(DEBUG_ALWAYS, LOG_ERR, "Write failed: %s", str_vmnet_status(result)); + return -1; + } + + return pkt_count ? buflen : 00; +} + +const char *str_vmnet_status(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"; + case VMNET_SHARING_SERVICE_BUSY: + return "conflict, sharing service is in use"; + default: + return "unknown vmnet error"; + } +} diff --git a/src/bsd/darwin/vmnet.h b/src/bsd/darwin/vmnet.h new file mode 100644 index 000000000..39360e7f1 --- /dev/null +++ b/src/bsd/darwin/vmnet.h @@ -0,0 +1,29 @@ +/* + * vmnet - Tun device emulation for Darwin + * Copyright (C) 2024 Eric Karge + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef VMNET_H +#define VMNET_H + +#include + +int macos_vmnet_open(void); +int macos_vmnet_close(int fd); +ssize_t macos_vmnet_write(uint8_t *buffer, size_t buflen); + +#endif diff --git a/src/bsd/device.c b/src/bsd/device.c index c0642f57c..af6274702 100644 --- a/src/bsd/device.c +++ b/src/bsd/device.c @@ -31,6 +31,10 @@ #include "darwin/tunemu.h" #endif +#ifdef ENABLE_VMNET +#include "darwin/vmnet.h" +#endif + #ifdef HAVE_NET_IF_UTUN_H #include #include @@ -51,6 +55,9 @@ typedef enum device_type { DEVICE_TYPE_TAP, #ifdef ENABLE_TUNEMU DEVICE_TYPE_TUNEMU, +#endif +#ifdef ENABLE_VMNET + DEVICE_TYPE_VMNET, #endif DEVICE_TYPE_UTUN, } device_type_t; @@ -59,7 +66,9 @@ int device_fd = -1; char *device = NULL; char *iface = NULL; static const char *device_info = "OS X utun device"; -#if defined(ENABLE_TUNEMU) +#if defined(ENABLE_VMNET) +static device_type_t device_type = DEVICE_TYPE_VMNET; +#elif defined(ENABLE_TUNEMU) static device_type_t device_type = DEVICE_TYPE_TUNEMU; #elif defined(HAVE_OPENBSD) || defined(HAVE_FREEBSD) || defined(HAVE_DRAGONFLY) static device_type_t device_type = DEVICE_TYPE_TUNIFHEAD; @@ -142,6 +151,13 @@ static bool setup_device(void) { device_type = DEVICE_TYPE_TUNEMU; } +#endif +#ifdef ENABLE_VMNET + else if(!strcasecmp(type, "vmnet")) { + device = xstrdup("vmnet"); + device_type = DEVICE_TYPE_VMNET; + } + #endif #ifdef HAVE_NET_IF_UTUN_H else if(!strcasecmp(type, "utun")) { @@ -171,7 +187,12 @@ static bool setup_device(void) { } } - if(routing_mode == RMODE_SWITCH && device_type != DEVICE_TYPE_TAP) { + if(routing_mode == RMODE_SWITCH + && device_type != DEVICE_TYPE_TAP +#ifdef ENABLE_VMNET + && device_type != DEVICE_TYPE_VMNET +#endif + ) { logger(DEBUG_ALWAYS, LOG_ERR, "Only tap devices support switch mode!"); return false; } @@ -197,6 +218,13 @@ static bool setup_device(void) { } break; #endif +#ifdef ENABLE_VMNET + + case DEVICE_TYPE_VMNET: { + device_fd = macos_vmnet_open(); + } + break; +#endif #ifdef HAVE_NET_IF_UTUN_H case DEVICE_TYPE_UTUN: @@ -314,6 +342,12 @@ static bool setup_device(void) { case DEVICE_TYPE_TUNEMU: device_info = "BSD tunemu device"; break; +#endif +#ifdef ENABLE_VMNET + + case DEVICE_TYPE_VMNET: + device_info = "macOS vmnet device"; + break; #endif } @@ -338,6 +372,12 @@ static void close_device(void) { tunemu_close(device_fd); break; #endif +#ifdef ENABLE_VMNET + + case DEVICE_TYPE_VMNET: + macos_vmnet_close(device_fd); + break; +#endif default: close(device_fd); @@ -424,6 +464,9 @@ static bool read_packet(vpn_packet_t *packet) { break; } +#ifdef ENABLE_VMNET + case DEVICE_TYPE_VMNET: +#endif case DEVICE_TYPE_TAP: if((inlen = read(device_fd, DATA(packet), MTU)) <= 0) { logger(DEBUG_ALWAYS, LOG_ERR, "Error while reading from %s %s: %s", device_info, @@ -510,6 +553,17 @@ static bool write_packet(vpn_packet_t *packet) { break; #endif +#ifdef ENABLE_VMNET + + case DEVICE_TYPE_VMNET: + if(macos_vmnet_write(DATA(packet), packet->len) < 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "Error while writing to %s %s: %s", device_info, + device, strerror(errno)); + return false; + } + + break; +#endif default: return false; diff --git a/src/tincd.c b/src/tincd.c index 4c33dc070..0fbe35bef 100644 --- a/src/tincd.c +++ b/src/tincd.c @@ -492,6 +492,9 @@ int main(int argc, char **argv) { #ifdef ENABLE_TUNEMU " tunemu" #endif +#ifdef ENABLE_VMNET + " vmnet" +#endif #ifdef HAVE_MINIUPNPC " miniupnpc" #endif diff --git a/test/integration/device.py b/test/integration/device.py index 1db6d2149..6c50000f7 100755 --- a/test/integration/device.py +++ b/test/integration/device.py @@ -28,6 +28,9 @@ def unknown_device_types( if Feature.TUNEMU not in features: yield "tunemu" + if Feature.VMNET not in features: + yield "vmnet" + if system != "Darwin": if not system.endswith("BSD"): yield "tunnohead" diff --git a/test/integration/testlib/proc.py b/test/integration/testlib/proc.py index 970c80cc5..c8e8fbe36 100755 --- a/test/integration/testlib/proc.py +++ b/test/integration/testlib/proc.py @@ -55,6 +55,7 @@ class Feature(Enum): READLINE = "readline" SANDBOX = "sandbox" TUNEMU = "tunemu" + VMNET = "vmnet" UML = "uml" VDE = "vde" WATCHDOG = "watchdog"