diff --git a/osdep/README.md b/osdep/README.md deleted file mode 100644 index a77297abd..000000000 --- a/osdep/README.md +++ /dev/null @@ -1,6 +0,0 @@ -OS-Dependent and OS-Interface Things -====== - -This folder contains stuff that interfaces with the base operating system -like Phy for network access and the various OS-specific Ethernet tap -drivers. diff --git a/osdep/rust-osdep.h b/osdep/rust-osdep.h index 54c4fee90..a2a8b841d 100644 --- a/osdep/rust-osdep.h +++ b/osdep/rust-osdep.h @@ -1,8 +1,11 @@ -#ifdef __APPLE__ #include #include +#include + +#ifdef __APPLE__ #include #include +#include #include #include #include diff --git a/rust-zerotier-service/src/fastudpsocket.rs b/rust-zerotier-service/src/fastudpsocket.rs index 377ec9982..8556fe71a 100644 --- a/rust-zerotier-service/src/fastudpsocket.rs +++ b/rust-zerotier-service/src/fastudpsocket.rs @@ -18,14 +18,11 @@ use num_traits::cast::AsPrimitive; use std::os::raw::c_int; use crate::osdep as osdep; -// -// A very low-level fast UDP socket that uses thread-per-core semantics to -// achieve maximum possible throughput. This will spawn a lot of threads but -// these threads will be inactive unless packets are being received with them. -// -// On most OSes this is by far the fastest way to handle incoming UDP except -// for bypassing the kernel's TCP/IP stack entirely. -// +/* + * This is a threaded UDP socket listener for high performance. The fastest way to receive UDP + * (without heroic efforts like kernel bypass) on most platforms is to create a separate socket + * for each thread using options like SO_REUSEPORT and concurrent packet listening. + */ #[cfg(windows)] use winapi::um::winsock2 as winsock2; @@ -125,33 +122,29 @@ fn bind_udp_socket(_: &str, address: &InetAddress) -> Result { - handler: Arc, +pub struct FastUDPSocket { threads: Vec>, thread_run: Arc, sockets: Vec, pub bind_address: InetAddress, } +/// Send to a raw UDP socket with optional packet TTL. +/// If the packet_ttl option is <=0, packet is sent with the default TTL. TTL setting is only used +/// in ZeroTier right now to do escalating TTL probes for IPv4 NAT traversal. #[cfg(unix)] #[inline(always)] pub fn fast_udp_socket_sendto(socket: &FastUDPRawOsSocket, to_address: &InetAddress, data: *const u8, len: usize, packet_ttl: i32) { unsafe { if packet_ttl <= 0 { - osdep::sendto(*socket, data.cast(), len as osdep::size_t, 0, (to_address as *const InetAddress).cast(), std::mem::size_of::() as osdep::socklen_t); + osdep::sendto(*socket, data.cast(), len.as_(), 0, (to_address as *const InetAddress).cast(), std::mem::size_of::().as_()); } else { let mut ttl = packet_ttl as c_int; - osdep::setsockopt(*socket, osdep::IPPROTO_IP.as_(), osdep::IP_TTL.as_(), (&mut ttl as *mut c_int).cast(), std::mem::size_of::() as osdep::socklen_t); - osdep::sendto(*socket, data.cast(), len as osdep::size_t, 0, (to_address as *const InetAddress).cast(), std::mem::size_of::() as osdep::socklen_t); + osdep::setsockopt(*socket, osdep::IPPROTO_IP.as_(), osdep::IP_TTL.as_(), (&mut ttl as *mut c_int).cast(), std::mem::size_of::().as_()); + osdep::sendto(*socket, data.cast(), len.as_(), 0, (to_address as *const InetAddress).cast(), std::mem::size_of::().as_()); ttl = 255; - osdep::setsockopt(*socket, osdep::IPPROTO_IP.as_(), osdep::IP_TTL.as_(), (&mut ttl as *mut c_int).cast(), std::mem::size_of::() as osdep::socklen_t); + osdep::setsockopt(*socket, osdep::IPPROTO_IP.as_(), osdep::IP_TTL.as_(), (&mut ttl as *mut c_int).cast(), std::mem::size_of::().as_()); } } } @@ -166,16 +159,15 @@ pub fn fast_udp_socket_sendto(socket: &FastUDPRawOsSocket, to_address: &InetAddr fn fast_udp_socket_recvfrom(socket: &FastUDPRawOsSocket, buf: &mut Buffer, from_address: &mut InetAddress) -> i32 { unsafe { let mut addrlen = std::mem::size_of::() as osdep::socklen_t; - osdep::recvfrom(*socket, buf.as_mut_ptr().cast(), Buffer::CAPACITY as osdep::size_t, 0, (from_address as *mut InetAddress).cast(), &mut addrlen) as i32 + osdep::recvfrom(*socket, buf.as_mut_ptr().cast(), Buffer::CAPACITY.as_(), 0, (from_address as *mut InetAddress).cast(), &mut addrlen) as i32 } } -impl FastUDPSocket { - pub fn new(device_name: &str, address: &InetAddress, handler: &Arc) -> Result, String> { - let thread_count = num_cpus::get(); +impl FastUDPSocket { + pub fn new(device_name: &str, address: &InetAddress, handler: F) -> Result { + let thread_count = num_cpus::get_physical().min(num_cpus::get()); let mut s = FastUDPSocket{ - handler: handler.clone(), thread_run: Arc::new(AtomicBool::new(true)), threads: Vec::new(), sockets: Vec::new(), @@ -190,20 +182,15 @@ impl FastUDPSocket { s.sockets.push(thread_socket); let thread_run = s.thread_run.clone(); - let handler_weak = Arc::downgrade(&s.handler); + let handler_copy = handler.clone(); s.threads.push(std::thread::Builder::new().stack_size(zerotier_core::RECOMMENDED_THREAD_STACK_SIZE).spawn(move || { let mut from_address = InetAddress::new(); while thread_run.load(Ordering::Relaxed) { let mut buf = Buffer::new(); let read_length = fast_udp_socket_recvfrom(&thread_socket, &mut buf, &mut from_address); if read_length > 0 { - let handler = handler_weak.upgrade(); - if handler.is_some() { - unsafe { buf.set_len(read_length as usize); } - handler.unwrap().incoming_udp_packet(&thread_socket, &from_address, buf); - } else { - break; - } + unsafe { buf.set_len(read_length as usize); } + handler_copy(&thread_socket, &from_address, buf); } else if read_length < 0 { break; } @@ -214,6 +201,9 @@ impl FastUDPSocket { } } + // This is successful if it is able to bind successfully once and launch at least one thread, + // since in a few cases it may be impossible to do multithreaded binding such as old Linux + // kernels or emulation layers. if s.sockets.is_empty() { return Err(format!("unable to bind to address for IPv4 or IPv6 ({})", bind_failed_reason)); } @@ -243,14 +233,10 @@ impl FastUDPSocket { } } -impl Drop for FastUDPSocket { +impl Drop for FastUDPSocket { #[cfg(windows)] fn drop(&mut self) { - self.thread_run.store(false, Ordering::Relaxed); // TODO - for t in self.threads.iter() { - t.join() - } } #[cfg(unix)] @@ -273,11 +259,12 @@ impl Drop for FastUDPSock } } while !self.threads.is_empty() { - self.threads.pop().unwrap().join().expect("unable to join to thread"); + let _ = self.threads.pop().unwrap().join(); } } } +/* #[cfg(test)] mod tests { use crate::fastudpsocket::*; @@ -336,3 +323,4 @@ mod tests { //println!("FastUDPSocket shutdown successful"); } } +*/ diff --git a/rust-zerotier-service/src/main.rs b/rust-zerotier-service/src/main.rs index dc4323cc3..f6a7735f3 100644 --- a/rust-zerotier-service/src/main.rs +++ b/rust-zerotier-service/src/main.rs @@ -17,7 +17,7 @@ mod physicallink; mod log; mod store; mod network; -mod vnp; +mod vnic; #[allow(non_snake_case,non_upper_case_globals,non_camel_case_types,dead_code,improper_ctypes)] mod osdep; @@ -47,12 +47,6 @@ use crate::network::Network; pub struct ServiceEventHandler {} -impl FastUDPSocketPacketHandler for ServiceEventHandler { - #[inline(always)] - fn incoming_udp_packet(&self, raw_socket: &FastUDPRawOsSocket, from_adddress: &InetAddress, data: Buffer) { - } -} - impl NodeEventHandler for ServiceEventHandler { fn virtual_network_config(&self, network_id: NetworkId, network_obj: &Arc, config_op: VirtualNetworkConfigOperation, config: Option<&VirtualNetworkConfig>) { } @@ -99,7 +93,7 @@ fn main() { let tokio_rt = tokio::runtime::Builder::new_multi_thread().thread_stack_size(zerotier_core::RECOMMENDED_THREAD_STACK_SIZE).build().unwrap(); tokio_rt.block_on(async { // Keeps track of FastUDPSocket instances by bound address. - let mut udp_sockets: BTreeMap> = BTreeMap::new(); + let mut udp_sockets: BTreeMap = BTreeMap::new(); // Send something to interrupt_tx to interrupt the inner loop and force it to // detect a change or exit if run has been set to false. @@ -194,7 +188,9 @@ fn main() { // Bind addresses that are not already bound. for addr in system_addrs.iter() { if !udp_sockets.contains_key(addr.0) { - let s = FastUDPSocket::new(addr.1.device.as_str(), addr.0, &handler); + let s = FastUDPSocket::new(addr.1.device.as_str(), addr.0, |raw_socket: &FastUDPRawOsSocket, from_address: &InetAddress, data: Buffer| { + // TODO + }); if s.is_ok() { udp_sockets.insert(addr.0.clone(), s.unwrap()); } else if addr.0.port() == local_config.settings.primary_port { diff --git a/rust-zerotier-service/src/physicallink.rs b/rust-zerotier-service/src/physicallink.rs index 44747796f..563f3d3c4 100644 --- a/rust-zerotier-service/src/physicallink.rs +++ b/rust-zerotier-service/src/physicallink.rs @@ -24,8 +24,8 @@ pub struct PhysicalLink { } #[inline(always)] -fn s6_addr_as_ptr(a: &A) -> *A { - a as *A +fn s6_addr_as_ptr(a: &A) -> *const A { + a as *const A } impl PhysicalLink { @@ -39,10 +39,10 @@ impl PhysicalLink { if !(*i).ifa_addr.is_null() { let mut a = InetAddress::new(); - let sa_family = (*(*i).ifa_addr).sa_family; - if sa_family == osdep::AF_INET.as_() { + let sa_family = (*(*i).ifa_addr).sa_family as u8; + if sa_family == osdep::AF_INET as u8 { copy_nonoverlapping((*i).ifa_addr.cast::(), (&mut a as *mut InetAddress).cast::(), size_of::()); - } else if sa_family == osdep::AF_INET6.as_() { + } else if sa_family == osdep::AF_INET6 as u8 { copy_nonoverlapping((*i).ifa_addr.cast::(), (&mut a as *mut InetAddress).cast::(), size_of::()); } else { continue; @@ -50,10 +50,10 @@ impl PhysicalLink { let mut netmask_bits: u16 = 0; if !(*i).ifa_netmask.is_null() { - if sa_family == osdep::AF_INET.as_() { + if sa_family == osdep::AF_INET as u8 { let mut a = (*(*i).ifa_netmask.cast::()).sin_addr.s_addr as u32; netmask_bits = a.leading_ones() as u16; - } else if sa_family == osdep::AF_INET6.as_() { + } else if sa_family == osdep::AF_INET6 as u8 { let a = s6_addr_as_ptr(&((*(*i).ifa_netmask.cast::()).sin6_addr)).cast::(); for i in 0..16 as isize { let mut b = *a.offset(i); diff --git a/rust-zerotier-service/src/vnp/mac_feth_tap.rs b/rust-zerotier-service/src/vnic/mac_feth_tap.rs similarity index 65% rename from rust-zerotier-service/src/vnp/mac_feth_tap.rs rename to rust-zerotier-service/src/vnic/mac_feth_tap.rs index d863f5825..613832439 100644 --- a/rust-zerotier-service/src/vnp/mac_feth_tap.rs +++ b/rust-zerotier-service/src/vnic/mac_feth_tap.rs @@ -22,22 +22,24 @@ */ use std::cell::Cell; +use std::collections::BTreeSet; use std::error::Error; use std::ffi::CString; -use std::os::raw::{c_int, c_ulong, c_void}; +use std::ptr::{null_mut, copy_nonoverlapping}; +use std::mem::transmute; +use std::os::raw::{c_int, c_uchar, c_ulong, c_void}; use std::process::Command; use std::sync::Mutex; use std::thread::JoinHandle; -use std::intrinsics::copy_nonoverlapping; use lazy_static::lazy_static; +use num_traits::cast::AsPrimitive; -use zerotier_core::{NetworkId, MAC, InetAddress, MulticastGroup}; +use zerotier_core::{InetAddress, MAC, MulticastGroup, NetworkId}; use crate::osdep as osdep; use crate::physicallink::PhysicalLink; -use crate::vnp::Port; -use std::collections::BTreeSet; +use crate::vnic::VNIC; const BPF_BUFFER_SIZE: usize = 131072; const IFCONFIG: &str = "/sbin/ifconfig"; @@ -66,7 +68,9 @@ impl Drop for MacFethDevice { pub struct MacFethTap { network_id: u64, device: MacFethDevice, + ndrv_fd: c_int, bpf_fd: c_int, + bpf_no: u32, bpf_read_thread: Cell>>, } @@ -74,14 +78,14 @@ pub struct MacFethTap { // #define BPF_WORDALIGN(x) (((x)+(BPF_ALIGNMENT-1))&~(BPF_ALIGNMENT-1)) // ... and also ... // #define BPF_ALIGNMENT sizeof(int32_t) -#[inline(always)] #[allow(non_snake_case)] -pub fn BPF_WORDALIGN(x: isize) -> isize { +#[inline(always)] +fn BPF_WORDALIGN(x: isize) -> isize { (((x + 3) as usize) & (!(3 as usize))) as isize } lazy_static! { - static ref MAC_FETH_GLOBAL_LOCK: Mutex = Mutex::new(0_i32); + static ref MAC_FETH_BPF_DEVICES_USED: Mutex> = Mutex::new(BTreeSet::new()); } impl MacFethTap { @@ -92,7 +96,10 @@ impl MacFethTap { /// from another thread that is spawned here, so all its bound references must /// be "Send" and "Sync" e.g. Arc<>. pub fn new(nwid: &NetworkId, mac: &MAC, mtu: i32, metric: i32, eth_frame_func: F) -> Result { - let _one_at_a_time = unsafe { MAC_FETH_GLOBAL_LOCK.lock().unwrap() }; + // This tracks BPF devices we are using so we don't try to reopen them, and also + // doubles as a global lock to ensure that only one feth tap is created at once per + // ZeroTier process per system. + let mut bpf_devices_used = MAC_FETH_BPF_DEVICES_USED.lock().unwrap(); if unsafe { osdep::getuid() } != 0 { return Err(String::from("ZeroTier MacFethTap must run as root")); @@ -165,15 +172,19 @@ impl MacFethTap { let mut bpf_no: u32 = 1; // start at 1 since some software hard-codes /dev/bpf0 let mut bpf_fd: c_int = -1; loop { - let bpf_dev = CString::new(format!("/dev/bpf{}", bpf_no)).unwrap(); - let bpf_dev = bpf_dev.as_bytes_with_nul(); - bpf_fd = unsafe { osdep::open(bpf_dev.as_ptr().cast(), osdep::O_RDWR as c_int) }; - if bpf_fd >= 0 { - break; - } - bpf_no += 1; - if bpf_no > 1000 { - break; + if bpf_devices_used.contains(&bpf_no) { + bpf_no += 1; + } else { + let bpf_dev = CString::new(format!("/dev/bpf{}", bpf_no)).unwrap(); + let bpf_dev = bpf_dev.as_bytes_with_nul(); + bpf_fd = unsafe { osdep::open(bpf_dev.as_ptr().cast(), osdep::O_RDWR as c_int) }; + if bpf_fd >= 0 { + break; + } + bpf_no += 1; + if bpf_no > 1000 { + break; + } } } if bpf_fd < 0 { @@ -206,7 +217,7 @@ impl MacFethTap { let mut bpf_ifr: osdep::ifreq = unsafe { std::mem::zeroed() }; let peer_dev_name_bytes = device.peer_name.as_bytes(); unsafe { copy_nonoverlapping(peer_dev_name_bytes.as_ptr(), bpf_ifr.ifr_name.as_mut_ptr().cast::(), if peer_dev_name_bytes.len() > (bpf_ifr.ifr_name.len() - 1) { bpf_ifr.ifr_name.len() - 1 } else { peer_dev_name_bytes.len() }); } - if unsafe { osdep::ioctl(bpf_fd as c_int, BIOCSETIF, (&mut bpf_ifr as *mut osdep::ifreq).cast::()) } != 0 { + if unsafe { osdep::ioctl(bpf_fd as c_int, osdep::c_BIOCSETIF, (&mut bpf_ifr as *mut osdep::ifreq).cast::()) } != 0 { unsafe { osdep::close(bpf_fd); } return Err(String::from("unable to configure BPF device")); } @@ -225,7 +236,8 @@ impl MacFethTap { return Err(String::from("unable to configure BPF device")); } - let t = std::thread::Builder::new().stack_size(zerotier_core::RECOMMENDED_THREAD_STACK_SIZE).spawn(|| { + // Create BPF listener thread, which calls the supplied function on each incoming packet. + let t = std::thread::Builder::new().stack_size(zerotier_core::RECOMMENDED_THREAD_STACK_SIZE).spawn(move || { let mut buf: [u8; BPF_BUFFER_SIZE] = [0_u8; BPF_BUFFER_SIZE]; let hdr_struct_size = std::mem::size_of::() as isize; loop { @@ -254,10 +266,37 @@ impl MacFethTap { return Err(String::from("unable to start thread")); } + // Create AF_NDRV socket used to inject packets. We could inject with BPF but that has + // a hard MTU limit of 2048 so we have to use AF_NDRV instead. Performance is probably + // the same, but it means another socket. + let ndrv_fd = unsafe { osdep::socket(osdep::AF_NDRV as c_int, osdep::SOCK_RAW as c_int, 0) }; + if ndrv_fd < 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to create AF_NDRV socket")); + } + let mut ndrv_sa: osdep::sockaddr_ndrv = unsafe { std::mem::zeroed() }; + ndrv_sa.snd_len = std::mem::size_of::() as c_uchar; + ndrv_sa.snd_family = osdep::AF_NDRV as c_uchar; + unsafe { copy_nonoverlapping(peer_dev_name_bytes.as_ptr(), ndrv_sa.snd_name.as_mut_ptr().cast::(), if peer_dev_name_bytes.len() > (bpf_ifr.ifr_name.len() - 1) { bpf_ifr.ifr_name.len() - 1 } else { peer_dev_name_bytes.len() }); } + if unsafe { osdep::bind(ndrv_fd, (&ndrv_sa as *const osdep::sockaddr_ndrv).cast(), std::mem::size_of::() as osdep::socklen_t) } != 0 { + unsafe { osdep::close(bpf_fd); } + unsafe { osdep::close(ndrv_fd); } + return Err(String::from("unable to bind AF_NDRV socket")); + } + if unsafe { osdep::connect(ndrv_fd, (&ndrv_sa as *const osdep::sockaddr_ndrv).cast(), std::mem::size_of::() as osdep::socklen_t) } != 0 { + unsafe { osdep::close(bpf_fd); } + unsafe { osdep::close(ndrv_fd); } + return Err(String::from("unable to connect AF_NDRV socket")); + } + + bpf_devices_used.insert(bpf_no); + Ok(MacFethTap { network_id: nwid.0, device: device, + ndrv_fd: ndrv_fd, bpf_fd: bpf_fd, + bpf_no: bpf_no, bpf_read_thread: Cell::new(Some(t.unwrap())) }) } @@ -265,7 +304,7 @@ impl MacFethTap { fn have_ip(&self, ip: &InetAddress) -> bool { let mut have_ip = false; PhysicalLink::map(|link: PhysicalLink| { - if link.device.eq(dev) && link.address.eq(ip) { + if link.device.eq(&self.device.name) && link.address.eq(ip) { have_ip = true; } }); @@ -273,7 +312,7 @@ impl MacFethTap { } } -impl Port for MacFethTap { +impl VNIC for MacFethTap { fn add_ip(&self, ip: &InetAddress) -> bool { if !self.have_ip(ip) { let cmd = Command::new(IFCONFIG).arg(&self.device.name).arg(if ip.is_v6() { "inet6" } else { "inet" }).arg(ip.to_string()).arg("alias").spawn(); @@ -315,9 +354,73 @@ impl Port for MacFethTap { } fn get_multicast_groups(&self) -> BTreeSet { - let groups: BTreeSet = BTreeSet::new(); + let dev = self.device.name.as_bytes(); + let mut groups: BTreeSet = BTreeSet::new(); + unsafe { + let mut maddrs: *mut osdep::ifmaddrs = null_mut(); + if osdep::getifmaddrs(&mut maddrs as *mut *mut osdep::ifmaddrs) == 0 { + let mut i = maddrs; + while !i.is_null() { + if !(*i).ifma_name.is_null() && !(*i).ifma_addr.is_null() && (*(*i).ifma_addr).sa_family == osdep::AF_LINK as osdep::sa_family_t { + let in_: &osdep::sockaddr_dl = &*((*i).ifma_name.cast()); + let la: &osdep::sockaddr_dl = &*((*i).ifma_addr.cast()); + if la.sdl_alen == 6 && in_.sdl_nlen <= dev.len() as osdep::u_char && osdep::memcmp(dev.as_ptr().cast(), in_.sdl_data.as_ptr().cast(), in_.sdl_nlen as c_ulong) == 0 { + let mi = la.sdl_nlen as usize; + groups.insert(MulticastGroup{ + mac: MAC( + (la.sdl_data[mi] as u64) << 40 | + (la.sdl_data[mi+1] as u64) << 32 | + (la.sdl_data[mi+2] as u64) << 24 | + (la.sdl_data[mi+3] as u64) << 16 | + (la.sdl_data[mi+4] as u64) << 8 | + la.sdl_data[mi+5] as u64 + ), + adi: 0, + }); + } + } + i = (*i).ifma_next; + } + osdep::freeifmaddrs(maddrs); + } + } groups } + + #[inline(always)] + fn put(&self, source_mac: &zerotier_core::MAC, dest_mac: &zerotier_core::MAC, ethertype: u16, vlan_id: u16, data: *const u8, len: usize) -> bool { + let dm = dest_mac.0; + let sm = source_mac.0; + let mut hdr: [u8; 14] = [ + (dm >> 40) as u8, + (dm >> 32) as u8, + (dm >> 24) as u8, + (dm >> 16) as u8, + (dm >> 8) as u8, + dm as u8, + (sm >> 40) as u8, + (sm >> 32) as u8, + (sm >> 24) as u8, + (sm >> 16) as u8, + (sm >> 8) as u8, + sm as u8, + (ethertype >> 8) as u8, + ethertype as u8 + ]; + unsafe { + let iov: [osdep::iovec; 2] = [ + osdep::iovec { + iov_base: hdr.as_mut_ptr().cast(), + iov_len: 14, + }, + osdep::iovec { + iov_base: transmute(data), // have to "cast away const" even though data is not modified by writev() + iov_len: len as osdep::size_t, + }, + ]; + osdep::writev(self.ndrv_fd, iov.as_ptr(), 2) == (len + 14) as osdep::ssize_t + } + } } impl Drop for MacFethTap { @@ -326,6 +429,12 @@ impl Drop for MacFethTap { unsafe { osdep::shutdown(self.bpf_fd, osdep::SHUT_RDWR as c_int); osdep::close(self.bpf_fd); + MAC_FETH_BPF_DEVICES_USED.lock().unwrap().remove(&self.bpf_no); + } + } + if self.ndrv_fd >= 0 { + unsafe { + osdep::close(self.ndrv_fd); } } let t = self.bpf_read_thread.replace(None); diff --git a/rust-zerotier-service/src/vnp/mod.rs b/rust-zerotier-service/src/vnic/mod.rs similarity index 68% rename from rust-zerotier-service/src/vnp/mod.rs rename to rust-zerotier-service/src/vnic/mod.rs index 69bac6fab..e2b4bf053 100644 --- a/rust-zerotier-service/src/vnp/mod.rs +++ b/rust-zerotier-service/src/vnic/mod.rs @@ -1,12 +1,11 @@ -// VNP = virtual network port, a.k.a. tun/tap device driver interface. - #[cfg(target_os = "macos")] mod mac_feth_tap; -pub trait Port { +pub trait VNIC { fn add_ip(&self, ip: &zerotier_core::InetAddress) -> bool; fn remove_ip(&self, ip: &zerotier_core::InetAddress) -> bool; fn ips(&self) -> Vec; fn device_name(&self) -> String; fn get_multicast_groups(&self) -> std::collections::BTreeSet; + fn put(&self, source_mac: &zerotier_core::MAC, dest_mac: &zerotier_core::MAC, ethertype: u16, vlan_id: u16, data: *const u8, len: usize) -> bool; }