diff options
Diffstat (limited to 'src/VirtualTap.cpp')
| -rw-r--r-- | src/VirtualTap.cpp | 541 |
1 files changed, 496 insertions, 45 deletions
diff --git a/src/VirtualTap.cpp b/src/VirtualTap.cpp index 88b6861..fd1b32a 100644 --- a/src/VirtualTap.cpp +++ b/src/VirtualTap.cpp @@ -14,28 +14,41 @@ /** * @file * - * Virtual Ethernet tap device + * Virtual ethernet tap device and combined network stack driver */ -#include "VirtualTap.hpp" -#include "Phy.hpp" -#include "Node.hpp" -//#include "OSUtils.hpp" - -#include "Service.hpp" +#include "MAC.hpp" #include "Mutex.hpp" -#include "lwipDriver.hpp" -#include "ZeroTier.h" +#include "InetAddress.hpp" +#include "MulticastGroup.hpp" + +#include "lwip/netif.h" +#include "lwip/etharp.h" +#include "lwip/sys.h" +#include "lwip/ethip6.h" +#include "lwip/tcpip.h" +#include "netif/ethernet.h" + +#ifdef LWIP_STATS +#include "lwip/stats.h" +#endif -#ifdef _MSC_VER +#include "VirtualTap.hpp" +#include "ZeroTierSockets.h" +#include "Events.hpp" +#include "Debug.hpp" + +#if defined(__WINDOWS__) +#include <time.h> #include "Synchapi.h" #endif +#define ZTS_TAP_THREAD_POLLING_INTERVAL 50 +#define LWIP_DRIVER_LOOP_INTERVAL 250 + namespace ZeroTier { -class VirtualTap; -extern OneService *service; -extern void postEvent(int eventCode, void *arg); +extern void _enqueueEvent(int16_t eventCode, void *arg = NULL); /** * A virtual tap device. The ZeroTier core service creates one of these for each @@ -66,7 +79,7 @@ VirtualTap::VirtualTap( memset(vtap_full_name, 0, sizeof(vtap_full_name)); snprintf(vtap_full_name, sizeof(vtap_full_name), "libzt%llx", (unsigned long long)_nwid); _dev = vtap_full_name; -#ifndef _WIN32 +#ifndef __WINDOWS__ ::pipe(_shutdownSignalPipe); #endif // Start virtual tap thread and stack I/O loops @@ -77,18 +90,18 @@ VirtualTap::~VirtualTap() { struct zts_network_details *nd = new zts_network_details; nd->nwid = _nwid; - postEvent(ZTS_EVENT_NETWORK_DOWN, (void*)nd); + _enqueueEvent(ZTS_EVENT_NETWORK_DOWN, (void*)nd); _run = false; -#ifndef _WIN32 +#ifndef __WINDOWS__ ::write(_shutdownSignalPipe[1],"\0",1); #endif _phy.whack(); - lwip_remove_netif(netif4); + _lwip_remove_netif(netif4); netif4 = NULL; - lwip_remove_netif(netif6); + _lwip_remove_netif(netif6); netif6 = NULL; Thread::join(_thread); -#ifndef _WIN32 +#ifndef __WINDOWS__ ::close(_shutdownSignalPipe[0]); ::close(_shutdownSignalPipe[1]); #endif @@ -156,7 +169,7 @@ bool VirtualTap::addIp(const InetAddress &ip) return false; } if (std::find(_ips.begin(),_ips.end(),ip) == _ips.end()) { - lwip_init_interface((void*)this, ip); + _lwip_init_interface((void*)this, ip); // TODO: Add ZTS_EVENT_ADDR_NEW ? _ips.push_back(ip); // Send callback message @@ -165,12 +178,12 @@ bool VirtualTap::addIp(const InetAddress &ip) if (ip.isV4()) { struct sockaddr_in *in4 = (struct sockaddr_in*)&(ad->addr); memcpy(&(in4->sin_addr.s_addr), ip.rawIpData(), 4); - postEvent(ZTS_EVENT_ADDR_ADDED_IP4, (void*)ad); + _enqueueEvent(ZTS_EVENT_ADDR_ADDED_IP4, (void*)ad); } if (ip.isV6()) { struct sockaddr_in6 *in6 = (struct sockaddr_in6*)&(ad->addr); memcpy(&(in6->sin6_addr.s6_addr), ip.rawIpData(), 16); - postEvent(ZTS_EVENT_ADDR_ADDED_IP6, (void*)ad); + _enqueueEvent(ZTS_EVENT_ADDR_ADDED_IP6, (void*)ad); } std::sort(_ips.begin(),_ips.end()); } @@ -187,14 +200,14 @@ bool VirtualTap::removeIp(const InetAddress &ip) if (ip.isV4()) { struct sockaddr_in *in4 = (struct sockaddr_in*)&(ad->addr); memcpy(&(in4->sin_addr.s_addr), ip.rawIpData(), 4); - postEvent(ZTS_EVENT_ADDR_REMOVED_IP4, (void*)ad); + _enqueueEvent(ZTS_EVENT_ADDR_REMOVED_IP4, (void*)ad); // FIXME: De-register from network stack } if (ip.isV6()) { // FIXME: De-register from network stack struct sockaddr_in6 *in6 = (struct sockaddr_in6*)&(ad->addr); memcpy(&(in6->sin6_addr.s6_addr), ip.rawIpData(), 16); - postEvent(ZTS_EVENT_ADDR_REMOVED_IP6, (void*)ad); + _enqueueEvent(ZTS_EVENT_ADDR_REMOVED_IP6, (void*)ad); } _ips.erase(i); } @@ -211,7 +224,7 @@ void VirtualTap::put(const MAC &from,const MAC &to,unsigned int etherType, const void *data,unsigned int len) { if (len <= _mtu && _enabled) { - lwip_eth_rx(this, from, to, etherType, data, len); + _lwip_eth_rx(this, from, to, etherType, data, len); } } @@ -220,19 +233,6 @@ std::string VirtualTap::deviceName() const return _dev; } -std::string VirtualTap::nodeId() const -{ - if (service) { - char id[16]; - memset(id, 0, sizeof(id)); - sprintf(id, "%llx", (unsigned long long)((OneService *)service)->getNode()->address()); - return std::string(id); - } - else { - return std::string("----------"); - } -} - void VirtualTap::setFriendlyName(const char *friendlyName) { DEBUG_INFO("%s", friendlyName); @@ -290,7 +290,7 @@ void VirtualTap::threadMain() if (FD_ISSET(_shutdownSignalPipe[0],&readfds)) { break; } -#if defined(_WIN32) +#if defined(__WINDOWS__) Sleep(ZTS_TAP_THREAD_POLLING_INTERVAL); #else struct timespec sleepValue = {0}; @@ -300,11 +300,6 @@ void VirtualTap::threadMain() } } -void VirtualTap::Housekeeping() -{ - // -} - void VirtualTap::phyOnDatagram(PhySocket *sock,void **uptr,const struct sockaddr *local_address, const struct sockaddr *from,void *data,unsigned long len) {} void VirtualTap::phyOnTcpConnect(PhySocket *sock,void **uptr,bool success) {} @@ -313,5 +308,461 @@ void VirtualTap::phyOnTcpAccept(PhySocket *sockL,PhySocket *sockN,void **uptrL,v void VirtualTap::phyOnTcpClose(PhySocket *sock,void **uptr) {} void VirtualTap::phyOnTcpData(PhySocket *sock,void **uptr,void *data,unsigned long len) {} void VirtualTap::phyOnTcpWritable(PhySocket *sock,void **uptr) {} +void VirtualTap::phyOnUnixClose(PhySocket *sock,void **uptr) {} + +////////////////////////////////////////////////////////////////////////////// +// Netif driver code for lwIP network stack // +////////////////////////////////////////////////////////////////////////////// + +bool _has_exited = false; + +// Used to generate enumerated lwIP interface names +int netifCount = 0; + +// Lock to guard access to network stack state changes +Mutex stackLock; + +// Callback for when the TCPIP thread has been successfully started +static void _tcpip_init_done(void *arg) +{ + sys_sem_t *sem; + sem = (sys_sem_t *)arg; + _setState(ZTS_STATE_STACK_RUNNING); + _enqueueEvent(ZTS_EVENT_STACK_UP); + sys_sem_signal(sem); +} + +static void _main_lwip_driver_loop(void *arg) +{ +#if defined(__linux__) + pthread_setname_np(pthread_self(), ZTS_LWIP_DRIVER_THREAD_NAME); +#endif +#if defined(__APPLE__) + pthread_setname_np(ZTS_LWIP_DRIVER_THREAD_NAME); +#endif + sys_sem_t sem; + LWIP_UNUSED_ARG(arg); + if (sys_sem_new(&sem, 0) != ERR_OK) { + DEBUG_ERROR("failed to create semaphore"); + } + tcpip_init(_tcpip_init_done, &sem); + sys_sem_wait(&sem); + // Main loop + while(_getState(ZTS_STATE_STACK_RUNNING)) { + zts_delay_ms(LWIP_DRIVER_LOOP_INTERVAL); + } + _has_exited = true; + _enqueueEvent(ZTS_EVENT_STACK_DOWN); +} + +bool _lwip_is_up() +{ + Mutex::Lock _l(stackLock); + return _getState(ZTS_STATE_STACK_RUNNING); +} + +bool _lwip_has_previously_shutdown() +{ + Mutex::Lock _l(stackLock); + return _has_exited; +} + +void _lwip_driver_init() +{ + if (_lwip_is_up()) { + return; + } + if (_lwip_has_previously_shutdown()) { + return; + } + Mutex::Lock _l(stackLock); +#if defined(__WINDOWS__) + sys_init(); // Required for win32 init of critical sections +#endif + sys_thread_new(ZTS_LWIP_DRIVER_THREAD_NAME, _main_lwip_driver_loop, + NULL, DEFAULT_THREAD_STACKSIZE, DEFAULT_THREAD_PRIO); +} + +void _lwip_driver_shutdown() +{ + if (_lwip_has_previously_shutdown()) { + return; + } + Mutex::Lock _l(stackLock); + // Set flag to stop sending frames into the core + _clrState(ZTS_STATE_STACK_RUNNING); + // Wait until the main lwIP thread has exited + while (!_has_exited) { zts_delay_ms(LWIP_DRIVER_LOOP_INTERVAL); } + /* + if (tcpip_shutdown() == ERR_OK) { + sys_timeouts_free(); + } + */ +} + +void _lwip_remove_netif(void *netif) +{ + if (!netif) { + return; + } + struct netif *n = (struct netif*)netif; + LOCK_TCPIP_CORE(); + netif_remove(n); + netif_set_down(n); + netif_set_link_down(n); + UNLOCK_TCPIP_CORE(); +} + +err_t _lwip_eth_tx(struct netif *n, struct pbuf *p) +{ + if (!n) { + return ERR_IF; + } + struct pbuf *q; + char buf[ZT_MAX_MTU+32]; + char *bufptr; + int totalLength = 0; + + VirtualTap *tap = (VirtualTap*)n->state; + bufptr = buf; + for (q = p; q != NULL; q = q->next) { + memcpy(bufptr, q->payload, q->len); + bufptr += q->len; + totalLength += q->len; + } + struct eth_hdr *ethhdr; + ethhdr = (struct eth_hdr *)buf; + + MAC src_mac; + MAC dest_mac; + src_mac.setTo(ethhdr->src.addr, 6); + dest_mac.setTo(ethhdr->dest.addr, 6); + + char *data = buf + sizeof(struct eth_hdr); + int len = totalLength - sizeof(struct eth_hdr); + int proto = Utils::ntoh((uint16_t)ethhdr->type); + tap->_handler(tap->_arg, NULL, tap->_nwid, src_mac, dest_mac, proto, 0, data, len); + if (ZT_MSG_TRANSFER == true) { + char flagbuf[32]; + memset(&flagbuf, 0, 32); + char macBuf[ZTS_MAC_ADDRSTRLEN], nodeBuf[16]; + snprintf(macBuf, ZTS_MAC_ADDRSTRLEN, "%02x:%02x:%02x:%02x:%02x:%02x", + ethhdr->dest.addr[0], ethhdr->dest.addr[1], ethhdr->dest.addr[2], + ethhdr->dest.addr[3], ethhdr->dest.addr[4], ethhdr->dest.addr[5]); + MAC mac; + mac.setTo(ethhdr->dest.addr, 6); + mac.toAddress(tap->_nwid).toString(nodeBuf); + /* + DEBUG_TRANS("len=%5d dst=%s [%s TX <-- %s] ethertype=0x%04x %s", totalLength, macBuf, nodeBuf, tap->nodeId().c_str(), + Utils::ntoh(ethhdr->type), flagbuf); + */ + } + return ERR_OK; +} + +void _lwip_eth_rx(VirtualTap *tap, const MAC &from, const MAC &to, unsigned int etherType, + const void *data, unsigned int len) +{ +#ifdef LWIP_STATS + stats_display(); +#endif + if (!_getState(ZTS_STATE_STACK_RUNNING)) { + return; + } + struct pbuf *p,*q; + struct eth_hdr ethhdr; + from.copyTo(ethhdr.src.addr, 6); + to.copyTo(ethhdr.dest.addr, 6); + ethhdr.type = Utils::hton((uint16_t)etherType); + + if (ZT_MSG_TRANSFER == true) { + char flagbuf[32]; + memset(&flagbuf, 0, 32); + char macBuf[ZTS_MAC_ADDRSTRLEN], nodeBuf[16]; + snprintf(macBuf, ZTS_MAC_ADDRSTRLEN, "%02x:%02x:%02x:%02x:%02x:%02x", + ethhdr.dest.addr[0], ethhdr.dest.addr[1], ethhdr.dest.addr[2], + ethhdr.dest.addr[3], ethhdr.dest.addr[4], ethhdr.dest.addr[5]); + MAC mac; + mac.setTo(ethhdr.src.addr, 6); + mac.toAddress(tap->_nwid).toString(nodeBuf); + /* + DEBUG_TRANS("len=%5d dst=%s [%s RX --> %s] ethertype=0x%04x %s", len, macBuf, nodeBuf, tap->nodeId().c_str(), + Utils::ntoh(ethhdr.type), flagbuf); + */ + } + + p = pbuf_alloc(PBUF_RAW, (uint16_t)len+sizeof(struct eth_hdr), PBUF_RAM); + if (!p) { + DEBUG_ERROR("dropped packet: unable to allocate memory for pbuf"); + return; + } + // First pbuf gets ethernet header at start + q = p; + if (q->len < sizeof(ethhdr)) { + pbuf_free(p); + p = NULL; + DEBUG_ERROR("dropped packet: first pbuf smaller than ethernet header"); + return; + } + // Copy frame data into pbuf + const char *dataptr = reinterpret_cast<const char *>(data); + memcpy(q->payload,ðhdr,sizeof(ethhdr)); + int remainingPayloadSpace = q->len - sizeof(ethhdr); + memcpy((char*)q->payload + sizeof(ethhdr),dataptr,remainingPayloadSpace); + dataptr += remainingPayloadSpace; + // Remaining pbufs (if any) get rest of data + while ((q = q->next)) { + memcpy(q->payload,dataptr,q->len); + dataptr += q->len; + } + // Feed packet into stack + int err; + + if (Utils::ntoh(ethhdr.type) == 0x800 || Utils::ntoh(ethhdr.type) == 0x806) { + if ((err = ((struct netif *)tap->netif4)->input(p, (struct netif *)tap->netif4)) != ERR_OK) { + DEBUG_ERROR("packet input error (%d)", err); + pbuf_free(p); + } + } + if (Utils::ntoh(ethhdr.type) == 0x86DD) { + if ((err = ((struct netif *)tap->netif6)->input(p, (struct netif *)tap->netif6)) != ERR_OK) { + DEBUG_ERROR("packet input error (%d)", err); + pbuf_free(p); + } + } +} + +/* +static void print_netif_info(struct netif *n) { + DEBUG_INFO("n=%p, %c%c, %d, o=%p, o6=%p, mc=%x:%x:%x:%x:%x:%x, hwln=%d, st=%p, flgs=%d\n", + n, + n->name[0], + n->name[1], + n->mtu, + n->output, + n->output_ip6, + n->hwaddr[0], + n->hwaddr[1], + n->hwaddr[2], + n->hwaddr[3], + n->hwaddr[4], + n->hwaddr[5], + n->hwaddr_len, + n->state, + n->flags + ); +} +*/ + +bool _lwip_is_netif_up(void *n) +{ + if (!n) { + return false; + } + LOCK_TCPIP_CORE(); + bool result = netif_is_up((struct netif*)n); + UNLOCK_TCPIP_CORE(); + return result; +} + +/** + * Called when a netif is removed (ZTS_EVENT_NETIF_INTERFACE_REMOVED) + */ +#if LWIP_NETIF_REMOVE_CALLBACK +static void _netif_remove_callback(struct netif *n) +{ + // Called from core, no need to lock + if (!n || !n->state) { + return; + } + VirtualTap *tap = (VirtualTap *)n->state; + uint64_t mac = 0; + memcpy(&mac, n->hwaddr, n->hwaddr_len); + struct zts_netif_details *ifd = new zts_netif_details; + ifd->nwid = tap->_nwid; + memcpy(&(ifd->mac), n->hwaddr, n->hwaddr_len); + ifd->mac = lwip_htonl(ifd->mac) >> 16; + _enqueueEvent(ZTS_EVENT_NETIF_REMOVED, (void*)ifd); +} +#endif + +/** + * Called when a link is brought up or down (ZTS_EVENT_NETIF_LINK_UP, ZTS_EVENT_NETIF_LINK_DOWN) + */ +#if LWIP_NETIF_LINK_CALLBACK +static void _netif_link_callback(struct netif *n) +{ + // Called from core, no need to lock + if (!n || !n->state) { + return; + } + VirtualTap *tap = (VirtualTap *)n->state; + uint64_t mac = 0; + memcpy(&mac, n->hwaddr, n->hwaddr_len); + if (n->flags & NETIF_FLAG_LINK_UP) { + struct zts_netif_details *ifd = new zts_netif_details; + ifd->nwid = tap->_nwid; + memcpy(&(ifd->mac), n->hwaddr, n->hwaddr_len); + ifd->mac = lwip_htonl(ifd->mac) >> 16; + _enqueueEvent(ZTS_EVENT_NETIF_LINK_UP, (void*)ifd); + } + if (n->flags & NETIF_FLAG_LINK_UP) { + struct zts_netif_details *ifd = new zts_netif_details; + ifd->nwid = tap->_nwid; + memcpy(&(ifd->mac), n->hwaddr, n->hwaddr_len); + ifd->mac = lwip_htonl(ifd->mac) >> 16; + _enqueueEvent(ZTS_EVENT_NETIF_LINK_DOWN, (void*)ifd); + } +} +#endif + +void _lwip_set_callbacks(struct netif *n) +{ + if (!n) { + return; + } +#if LWIP_NETIF_STATUS_CALLBACK + // Not currently used + netif_set_status_callback(n, netif_status_callback); +#endif +#if LWIP_NETIF_REMOVE_CALLBACK + netif_set_remove_callback(n, netif_remove_callback); +#endif +#if LWIP_NETIF_LINK_CALLBACK + netif_set_link_callback(n, netif_link_callback); +#endif +} + +static struct zts_netif_details *_lwip_prepare_netif_status_msg(struct netif *n) +{ + if (!n || !n->state) { + return NULL; + } + VirtualTap *tap = (VirtualTap*)(n->state); + struct zts_netif_details *ifd = new zts_netif_details; + ifd->nwid = tap->_nwid; + ifd->mtu = n->mtu; + memcpy(&(ifd->mac), n->hwaddr, n->hwaddr_len); + ifd->mac = htonll(ifd->mac) >> 16; + return ifd; +} + +static err_t _netif_init4(struct netif *n) +{ + if (!n || !n->state) { + return ERR_IF; + } + // Called from netif code, no need to lock + n->hwaddr_len = 6; + n->name[0] = '4'; + n->name[1] = 'a'+netifCount; + n->linkoutput = _lwip_eth_tx; + n->output = etharp_output; + n->mtu = LWIP_MTU < ZT_MAX_MTU ? LWIP_MTU : ZT_MAX_MTU; + n->flags = NETIF_FLAG_BROADCAST + | NETIF_FLAG_ETHARP + | NETIF_FLAG_ETHERNET + | NETIF_FLAG_IGMP + | NETIF_FLAG_MLD6 + | NETIF_FLAG_LINK_UP + | NETIF_FLAG_UP; + n->hwaddr_len = sizeof(n->hwaddr); + VirtualTap *tap = (VirtualTap*)(n->state); + tap->_mac.copyTo(n->hwaddr, n->hwaddr_len); + return ERR_OK; +} + +static err_t _netif_init6(struct netif *n) +{ + if (!n || !n->state) { + return ERR_IF; + } + n->hwaddr_len = sizeof(n->hwaddr); + VirtualTap *tap = (VirtualTap*)(n->state); + tap->_mac.copyTo(n->hwaddr, n->hwaddr_len); + // Called from netif code, no need to lock + n->hwaddr_len = 6; + n->name[0] = '6'; + n->name[1] = 'a'+netifCount; + n->linkoutput = _lwip_eth_tx; + n->output_ip6 = ethip6_output; + n->mtu = LWIP_MTU < ZT_MAX_MTU ? LWIP_MTU : ZT_MAX_MTU; + n->flags = NETIF_FLAG_BROADCAST + | NETIF_FLAG_ETHARP + | NETIF_FLAG_ETHERNET + | NETIF_FLAG_IGMP + | NETIF_FLAG_MLD6 + | NETIF_FLAG_LINK_UP + | NETIF_FLAG_UP; + return ERR_OK; +} + +void _lwip_init_interface(void *tapref, const InetAddress &ip) +{ + char ipbuf[INET6_ADDRSTRLEN]; + char macbuf[ZTS_MAC_ADDRSTRLEN]; + + VirtualTap *vtap = (VirtualTap*)tapref; + struct netif *n = NULL; + bool isNewNetif = false; + + if (ip.isV4()) { + if (vtap->netif4) { + n = (struct netif*)vtap->netif4; + } + else { + n = new struct netif; + isNewNetif = true; + netifCount++; + } + char nmbuf[INET6_ADDRSTRLEN]; + static ip4_addr_t ip4, netmask, gw; + IP4_ADDR(&gw,127,0,0,1); + ip4.addr = *((u32_t *)ip.rawIpData()); + netmask.addr = *((u32_t *)ip.netmask().rawIpData()); + LOCK_TCPIP_CORE(); + netif_add(n, &ip4, &netmask, &gw, (void*)vtap, _netif_init4, tcpip_input); + vtap->netif4 = (void*)n; + _enqueueEvent(ZTS_EVENT_NETIF_UP, (void*)_lwip_prepare_netif_status_msg(n)); + UNLOCK_TCPIP_CORE(); + snprintf(macbuf, ZTS_MAC_ADDRSTRLEN, "%02x:%02x:%02x:%02x:%02x:%02x", + n->hwaddr[0], n->hwaddr[1], n->hwaddr[2], + n->hwaddr[3], n->hwaddr[4], n->hwaddr[5]); + DEBUG_INFO("initialized netif=%p as [mac=%s, addr=%s, nm=%s, tap=%p]",n, + macbuf, ip.toString(ipbuf), ip.netmask().toString(nmbuf), vtap); + } + if (ip.isV6()) { + if (vtap->netif6) { + n = (struct netif*)vtap->netif6; + } + else { + n = new struct netif; + isNewNetif = true; + netifCount++; + } + static ip6_addr_t ip6; + memcpy(&(ip6.addr), ip.rawIpData(), sizeof(ip6.addr)); + LOCK_TCPIP_CORE(); + if (isNewNetif) { + vtap->netif6 = (void*)n; + netif_add(n, NULL, NULL, NULL, (void*)vtap, _netif_init6, ethernet_input); + n->ip6_autoconfig_enabled = 1; + vtap->_mac.copyTo(n->hwaddr, n->hwaddr_len); + netif_create_ip6_linklocal_address(n, 1); + netif_set_link_up(n); + netif_set_up(n); + netif_set_default(n); + } + netif_add_ip6_address(n,&ip6,NULL); + n->output_ip6 = ethip6_output; + UNLOCK_TCPIP_CORE(); + _enqueueEvent(ZTS_EVENT_NETIF_UP, (void*)_lwip_prepare_netif_status_msg(n)); + snprintf(macbuf, ZTS_MAC_ADDRSTRLEN, "%02x:%02x:%02x:%02x:%02x:%02x", + n->hwaddr[0], n->hwaddr[1], n->hwaddr[2], + n->hwaddr[3], n->hwaddr[4], n->hwaddr[5]); + DEBUG_INFO("initialized netif=%p as [mac=%s, addr=%s, tap=%p]", n, + macbuf, ip.toString(ipbuf), vtap); + } +} -} // namespace ZeroTier
\ No newline at end of file +} // namespace ZeroTier |
