diff --git a/0readme_ethernet.txt b/0readme_ethernet.txt index 62357a7ca..0d8910c77 100644 --- a/0readme_ethernet.txt +++ b/0readme_ethernet.txt @@ -213,6 +213,79 @@ Note: As mentioned above, NAT networking is specifically capable of providing packets. +------------------------------------------------------------------------------- +Another alternative to pcap and tun/tap on macOS is using the vmnet framework. +The vmnet framework provides simh a way to provide emulated systems networking, +without requiring any additional software. Because tun/tap support requires +third-party kernel extensions (which is heavily discouraged by Apple), this +provides an equivalent that doesn't require reducing OS security. + +vmnet provides Layer 2 networking, so things like DECnet and MOP will work +properly, even across multiple systems. It can be used to either bridge to a +physical network, or provide a virtual network for systems, including NAT. + +simh must be running as root. (Running without root is possible if simh is a +notarized application. You're very unlikely to be building simh like this, so +de facto it needs root.) vmnet.framework requires macOS 10.10; macOS 10.15 is +required for bridged networking. + +When attaching an ethernet device, you have three options available: + + - "vmnet:host": This provides "host-only" networking. It allows the system to + talk to the host Mac, as well as other emulated systems on the host-only VM + network. + - "vmnet:shared": This extends the host-only network to add NAT, using the + host as a gateway. + - "vmnet:": This bridges vmnet to a physical network. You must + specify a valid network device to use. + + You can use SHOW ETHERNET in SCP to see what devices are allowed to be + bridged; these are likely your Ethernet and Wi-Fi devices. + + sim> show eth + ETH devices: + eth0 vmnet:en0 + eth1 vmnet:en7 + eth2 udp:sourceport:remotehost:remoteport + sim> attach xq vmnet:en0 + + You can verify with i.e. "ifconfig" to see if this is the correct interface + to bridge to. + +When using the host and shared modes, the host system create an IPv4 and v6 +subnet for the emulated systems. macOS will provide DHCP and other discovery +protocols on the virtual network as well. To see what IP and subnet the host +has claimed, you can use "ifconfig" to display it; macOS will bind its IP to +the bridgeN device, with the simh instance having its own vmenetN device joined +to that bridgeN device: + +vmenet0: flags=8963 mtu 1500 + options=60 + ether a6:94:63:7e:f3:04 + media: autoselect + status: active +bridge100: flags=8a63 mtu 1500 + options=3 + ether b2:f1:d8:65:66:64 + inet 192.168.67.1 netmask 0xffffff00 broadcast 192.168.67.255 + inet6 fe80::b0f1:d8ff:fe65:6664%bridge100 prefixlen 64 scopeid 0x23 + inet6 fd91:d62b:63cd:ec3d:4ad:c40c:d0b8:5826 prefixlen 64 autoconf secured + Configuration: + id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 + maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 + root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 + ipfilter disabled flags 0x0 + member: vmenet0 flags=3 + ifmaxaddr 0 port 34 priority 0 path cost 0 + nd6 options=201 + media: autoselect + status: active + +Currently, settings like the subnet ranges nor port forwarding are controllable +via simh. For finer grained control over how systems are addressed, you can use +a bridged network instead of a host-only or shared network. These settings are +provided by macOS, so a future version of simh may be able to adopt these. + ------------------------------------------------------------------------------- Windows notes: diff --git a/CMakeLists.txt b/CMakeLists.txt index 120da3960..c206d4f00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,6 +199,9 @@ option(WITH_VDE option(WITH_TAP "Enable (=1)/disable (=0) TAP/TUN device network support. (def: enabled)" TRUE) +option(WITH_VMNET + "Enable (=1)/disable (=0) macOS vmnet.framework network support. (def: enabled)" + TRUE) option(WITH_VIDEO "Enable (=1)/disable (=0) simulator display and graphics support (def: enabled)" TRUE) diff --git a/cmake/dep-link.cmake b/cmake/dep-link.cmake index 90c7707e0..0dec7bcf6 100644 --- a/cmake/dep-link.cmake +++ b/cmake/dep-link.cmake @@ -351,6 +351,36 @@ if (WITH_NETWORK) list(APPEND NETWORK_PKG_STATUS "NAT(SLiRP)") endif (WITH_SLIRP) + if (WITH_VMNET AND APPLE) + # CMAKE_OSX_DEPLOYMENT_TARGET is attractive, but not set by default. + # See what we're using, either by default or what the user has set. + check_c_source_compiles( + " + #include + #if TARGET_OS_OSX && __MAC_OS_X_VERSION_MIN_REQUIRED < 101000 + #error macOS too old + #endif + int main(int argc, char **argv) { return 0; } + " TARGETING_MACOS_10_10) + if (NOT TARGETING_MACOS_10_10) + message(FATAL_ERROR "vmnet.framework requires targeting macOS 10.10 or newer") + endif() + + # vmnet requires blocks for its callback parameter, even in vanilla C. + # This is only supported in clang, not by GCC. It's default in clang, + # but we should be clear about it. Do a feature instead of compiler + # check anyways though, in case GCC does add it eventually. + check_c_compiler_flag(-fblocks HAVE_C_BLOCKS) + if (NOT HAVE_C_BLOCKS) + message(FATAL_ERROR "vmnet.framework requires blocks support in the C compiler") + endif() + target_compile_options(simh_network INTERFACE -fblocks) + + target_link_libraries(simh_network INTERFACE "-framework vmnet") + target_compile_definitions(simh_network INTERFACE HAVE_VMNET_NETWORK) + list(APPEND NETWORK_PKG_STATUS "macOS vmnet.framework") + endif(WITH_VMNET AND APPLE) + ## Finally, set the network runtime if (NOT network_runtime) ## Default to USE_SHARED... USE_NETWORK is deprecated. diff --git a/makefile b/makefile index 79755eb24..f22c3bc2c 100644 --- a/makefile +++ b/makefile @@ -1059,6 +1059,13 @@ ifeq (${WIN32},) #*nix Environments (&& cygwin) NETWORK_CCDEFS += -DUSE_NETWORK endif endif + # XXX: Check for the target version of macOS, check for -fblocks + ifeq (Darwin,$(OSTYPE)) + # Provide support for macOS vmnet.framework (requires 10.10) + NETWORK_CCDEFS += -fblocks -DHAVE_VMNET_NETWORK + NETWORK_LAN_FEATURES += vmnet.framework + NETWORK_LDFLAGS += -framework vmnet + endif ifeq (slirp,$(shell if ${TEST} -e slirp_glue/sim_slirp.c; then echo slirp; fi)) NETWORK_CCDEFS += -Islirp -Islirp_glue -Islirp_glue/qemu -DHAVE_SLIRP_NETWORK -DUSE_SIMH_SLIRP_DEBUG slirp/*.c slirp_glue/*.c NETWORK_LAN_FEATURES += NAT(SLiRP) diff --git a/sim_ether.c b/sim_ether.c index 7faabbd3f..089ced2e0 100644 --- a/sim_ether.c +++ b/sim_ether.c @@ -381,6 +381,11 @@ #define MAX(a,b) (((a) > (b)) ? (a) : (b)) +// Declare earlier than other implementations +#ifdef HAVE_VMNET_NETWORK +#include +#endif + /* Internal routine - forward declaration */ static int _eth_get_system_id (char *buf, size_t buf_size); static void eth_get_nic_hw_addr(ETH_DEV* dev, const char *devname, int set_on); @@ -1005,6 +1010,9 @@ const char *eth_capabilities(void) #endif #if defined (HAVE_SLIRP_NETWORK) ":NAT" +#endif +#if defined (HAVE_VMNET_NETWORK) + ":VMNET" #endif ":UDP"; } @@ -1196,6 +1204,42 @@ if (used < max) { ++used; } #endif +#ifdef HAVE_VMNET_NETWORK +if (used < max) { + sprintf(list[used].name, "%s", "vmnet:shared"); + sprintf(list[used].desc, "%s", "Integrated NAT (vmnet.framework) support"); + list[used].eth_api = ETH_API_VMN; + ++used; +} +if (used < max) { + sprintf(list[used].name, "%s", "vmnet:host"); + sprintf(list[used].desc, "%s", "Integrated host-only network (vmnet.framework) support"); + list[used].eth_api = ETH_API_VMN; + ++used; +} +// vmnet.framework has an allowed list of devices for bridging +// handy for user if we list them since ifconfig is noisy +#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500) +if (__builtin_available(macOS 10.15, *)) { + // avoid putting a __block marker on used on other platforms + __block int used_block; + xpc_object_t bridge_list = vmnet_copy_shared_interface_list(); + + xpc_array_apply(bridge_list, ^bool(size_t index, xpc_object_t value) { + if (used_block < max) { + sprintf(list[used_block].name, "%s%s", "vmnet:", xpc_string_get_string_ptr(value)); + sprintf(list[used_block].desc, "%s", "Integrated bridged network (vmnet.framework) support"); + list[used_block].eth_api = ETH_API_VMN; + ++used_block; + } + return true; + }); + used = used_block; + + xpc_release(bridge_list); +} +#endif +#endif if (used < max) { sprintf(list[used].name, "%s", "udp:sourceport:remotehost:remoteport"); @@ -1761,6 +1805,10 @@ return tool; static void eth_get_nic_hw_addr(ETH_DEV* dev, const char *devname, int set_on) { + // we set this at interface creation time in open_port for vmnet + if (dev->eth_api == ETH_API_VMN) { + return; + } memset(&dev->host_nic_phy_hw_addr, 0, sizeof(dev->host_nic_phy_hw_addr)); dev->have_host_nic_phy_addr = 0; if (dev->eth_api != ETH_API_PCAP) @@ -2060,6 +2108,37 @@ while (dev->handle) { } break; #endif /* HAVE_VDE_NETWORK */ +#ifdef HAVE_VMNET_NETWORK + case ETH_API_VMN: + { + vmnet_return_t ret; + int count = 1; + struct pcap_pkthdr header; + struct vmpktdesc pkt_desc; + struct iovec iov; + + // XXX: Should be MTU returned from vmnet startup? + u_char buf[ETH_MAX_JUMBO_FRAME]; + + iov.iov_base = buf; + iov.iov_len = ETH_MAX_JUMBO_FRAME; + + pkt_desc.vm_pkt_size = ETH_MAX_JUMBO_FRAME; + pkt_desc.vm_pkt_iov = &iov; + pkt_desc.vm_pkt_iovcnt = 1; + pkt_desc.vm_flags = 0; + + ret = vmnet_read((interface_ref)dev->handle, &pkt_desc, &count); + if (ret == VMNET_SUCCESS && count > 0) { + status = 1; + header.caplen = header.len = pkt_desc.vm_pkt_size; + _eth_callback((u_char *)dev, &header, buf); + } else { + status = ret == VMNET_SUCCESS ? 0 : -1; + } + } + break; +#endif #ifdef HAVE_SLIRP_NETWORK case ETH_API_NAT: sim_slirp_dispatch ((SLIRP*)dev->handle); @@ -2244,6 +2323,87 @@ if (bufsz < ETH_MAX_JUMBO_FRAME) /* attempt to connect device */ memset(errbuf, 0, PCAP_ERRBUF_SIZE); + + if (0 == strncmp("vmnet:", savname, 6)) { +#if defined(HAVE_VMNET_NETWORK) + xpc_object_t if_desc; + dispatch_queue_t vmn_queue; + interface_ref vmn_interface; + // __block is used so it can be captured in the callback + __block vmnet_return_t vmn_status; + // Because vmnet operates via callbacks, set up a semaphore to block on + dispatch_semaphore_t cb_finished; + + const char *devname = savname + sizeof("vmnet:") - 1; + + if_desc = xpc_dictionary_create(NULL, NULL, 0); + if (0 == strcmp(devname, "shared")) { + xpc_dictionary_set_uint64(if_desc, vmnet_operation_mode_key, VMNET_SHARED_MODE); + } else if (0 == strcmp(devname, "host")) { + xpc_dictionary_set_uint64(if_desc, vmnet_operation_mode_key, VMNET_HOST_MODE); +#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500) + } else if (strlen(devname) > 0) { // Bridged, this is the device name + if (__builtin_available(macOS 10.15, *)) { + xpc_dictionary_set_uint64(if_desc, vmnet_operation_mode_key, VMNET_BRIDGED_MODE); + xpc_dictionary_set_string(if_desc, vmnet_shared_interface_name_key, devname); + } else { + xpc_release(if_desc); + return sim_messagef (SCPE_OPENERR, "Eth: You must be using macOS 10.15 or newer for bridged devices\n"); + } +#endif + } else { + if (__builtin_available(macOS 10.15, *)) { + xpc_release(if_desc); + return sim_messagef (SCPE_OPENERR, "Eth: You must pick either shared, host, or an allowed bridge device\n"); + } else { + xpc_release(if_desc); + return sim_messagef (SCPE_OPENERR, "Eth: You must pick either shared or host\n"); + } + } + + vmn_queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0); + + cb_finished = dispatch_semaphore_create(0); + + // Set the MAC address + __block ETH_DEV *eth_dev = (ETH_DEV*)opaque; + + vmn_interface = vmnet_start_interface(if_desc, vmn_queue, ^(vmnet_return_t status, xpc_object_t params){ + vmn_status = status; + + if (vmn_status == VMNET_SUCCESS) { + // Scan like eth_scan_mac but simplified (only one format) + const char *mac_string = xpc_dictionary_get_string(params, vmnet_mac_address_key); + int a[6]; + if (6 == sscanf(mac_string, "%x:%x:%x:%x:%x:%x", &a[0], &a[1], &a[2], &a[3], &a[4], &a[5])) { + eth_dev->have_host_nic_phy_addr = 1; + eth_dev->host_nic_phy_hw_addr[0] = a[0]; + eth_dev->host_nic_phy_hw_addr[1] = a[1]; + eth_dev->host_nic_phy_hw_addr[2] = a[2]; + eth_dev->host_nic_phy_hw_addr[3] = a[3]; + eth_dev->host_nic_phy_hw_addr[4] = a[4]; + eth_dev->host_nic_phy_hw_addr[5] = a[5]; + } + } + + dispatch_semaphore_signal(cb_finished); + }); + dispatch_semaphore_wait(cb_finished, DISPATCH_TIME_FOREVER); + dispatch_release(cb_finished); + xpc_release(if_desc); + + if (vmn_status != VMNET_SUCCESS) { + return sim_messagef (SCPE_OPENERR, "Eth: Failed to create vmnet (vmnet_return_t: %d)\n", vmn_status); + } + + *eth_api = ETH_API_VMN; + *handle = (void *)vmn_interface; /* Flag used to indicated open */ + return SCPE_OK; +#else + return sim_messagef (SCPE_OPENERR, "Eth: No support for vmnet devices\n"); +#endif + } + if (0 == strncmp("tap:", savname, 4)) { int tun = -1; /* TUN/TAP Socket */ int on = 1; @@ -2646,6 +2806,15 @@ switch (eth_api) { case ETH_API_NAT: sim_slirp_close((SLIRP*)pcap); break; +#endif +#ifdef HAVE_VMNET_NETWORK + case ETH_API_VMN: + { + dispatch_queue_t stop_queue; + stop_queue = dispatch_get_global_queue(QOS_CLASS_UTILITY, 0); + vmnet_stop_interface((interface_ref)pcap, stop_queue, ^(vmnet_return_t status){}); + } + break; #endif case ETH_API_UDP: sim_close_sock(pcap_fd); @@ -2743,6 +2912,11 @@ fprintf (st, " eth2 vde:device{:switch-port-number} (Integrated VDE su fprintf (st, " eth3 nat:{optional-nat-parameters} (Integrated NAT (SLiRP) support)\n"); #endif fprintf (st, " eth4 udp:sourceport:remotehost:remoteport (Integrated UDP bridge support)\n"); +#if defined(HAVE_VMNET_NETWORK) +fprintf (st, " eth6 vmnet:shared (Integrated NAT (vmnet.framework) support)\n"); +fprintf (st, " eth7 vmnet:host (Integrated host-only (vmnet.framework) support)\n"); +fprintf (st, " eth8 vmnet:device-name (Integrated bridged (vmnet.framework) support)\n"); +#endif fprintf (st, " sim> ATTACH %s eth0\n\n", dptr->name); fprintf (st, "or equivalently:\n\n"); fprintf (st, " sim> ATTACH %s en0\n\n", dptr->name); @@ -2960,6 +3134,9 @@ switch (dev->eth_api) { case ETH_API_NAT: netname = "nat"; break; + case ETH_API_VMN: + netname = "vmnet"; + break; } sprintf(msg, "%s(%s): ", where, netname); switch (dev->eth_api) { @@ -2967,6 +3144,12 @@ switch (dev->eth_api) { case ETH_API_PCAP: sim_printf ("%s%s\n", msg, pcap_geterr ((pcap_t*)dev->handle)); break; +#endif +#if defined(HAVE_VMNET_NETWORK) + case ETH_API_VMN: + /* XXX: vmnet errors aren't global */ + sim_printf ("%s\n", msg); + break; #endif default: sim_err_sock (INVALID_SOCKET, msg); @@ -3088,6 +3271,27 @@ if ((packet->len >= ETH_MIN_PACKET) && (packet->len <= ETH_MAX_PACKET)) { else status = 1; break; +#endif +#ifdef HAVE_VMNET_NETWORK + case ETH_API_VMN: + { + vmnet_return_t ret; + int count = 1; + struct vmpktdesc pkt_desc; + struct iovec iov; + + iov.iov_base = packet->msg; + iov.iov_len = packet->len; + + pkt_desc.vm_pkt_size = packet->len; + pkt_desc.vm_pkt_iov = &iov; + pkt_desc.vm_pkt_iovcnt = 1; + pkt_desc.vm_flags = 0; + + ret = vmnet_write((interface_ref)dev->handle, &pkt_desc, &count); + status = (ret == VMNET_SUCCESS && count > 0) ? 0 : 1; + } + break; #endif case ETH_API_UDP: status = (((int32)packet->len == sim_write_sock (dev->fd_handle, (char *)packet->msg, (int32)packet->len)) ? 0 : -1); @@ -3712,6 +3916,7 @@ switch (dev->eth_api) { case ETH_API_VDE: case ETH_API_UDP: case ETH_API_NAT: + case ETH_API_VMN: bpf_used = 0; to_me = 0; eth_packet_trace (dev, data, header->len, "received"); @@ -3908,6 +4113,37 @@ do { } break; #endif /* HAVE_VDE_NETWORK */ +#ifdef HAVE_VMNET_NETWORK + case ETH_API_VMN: + { + vmnet_return_t ret; + int count = 1; + struct pcap_pkthdr header; + struct vmpktdesc pkt_desc; + struct iovec iov; + + // XXX: Should be MTU returned from vmnet startup? + u_char buf[ETH_MAX_JUMBO_FRAME]; + + iov.iov_base = buf; + iov.iov_len = ETH_MAX_JUMBO_FRAME; + + pkt_desc.vm_pkt_size = ETH_MAX_JUMBO_FRAME; + pkt_desc.vm_pkt_iov = &iov; + pkt_desc.vm_pkt_iovcnt = 1; + pkt_desc.vm_flags = 0; + + ret = vmnet_read((interface_ref)dev->handle, &pkt_desc, &count); + if (ret == VMNET_SUCCESS && count > 0) { + status = 1; + header.caplen = header.len = pkt_desc.vm_pkt_size; + _eth_callback((u_char *)dev, &header, buf); + } else { + status = ret == VMNET_SUCCESS ? 0 : -1; + } + } + break; +#endif case ETH_API_UDP: if (1) { struct pcap_pkthdr header; diff --git a/sim_ether.h b/sim_ether.h index e3161a9a4..fe2555329 100644 --- a/sim_ether.h +++ b/sim_ether.h @@ -265,6 +265,7 @@ struct eth_device { #define ETH_API_VDE 3 /* VDE API in use */ #define ETH_API_UDP 4 /* UDP API in use */ #define ETH_API_NAT 5 /* NAT (SLiRP) API in use */ +#define ETH_API_VMN 6 /* Apple vmnet.framework in use */ ETH_PCALLBACK read_callback; /* read callback function */ ETH_PCALLBACK write_callback; /* write callback function */ ETH_PACK* read_packet; /* read packet */