/** * @file src/platform/macos/misc.mm * @brief Miscellaneous definitions for macOS platform. */ // Required for IPV6_PKTINFO with Darwin headers #ifndef __APPLE_USE_RFC_3542 // NOLINT(bugprone-reserved-identifier) #define __APPLE_USE_RFC_3542 1 #endif #include #include #include #include #include #include #include #include #include "misc.h" #include "src/entry_handler.h" #include "src/logging.h" #include "src/platform/common.h" #include #include using namespace std::literals; namespace fs = std::filesystem; namespace bp = boost::process; namespace platf { // Even though the following two functions are available starting in macOS 10.15, they weren't // actually in the Mac SDK until Xcode 12.2, the first to include the SDK for macOS 11 #if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // __MAC_11_0 // If they're not in the SDK then we can use our own function definitions. // Need to use weak import so that this will link in macOS 10.14 and earlier extern "C" bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import)); extern "C" bool CGRequestScreenCaptureAccess(void) __attribute__((weak_import)); #endif std::unique_ptr init() { // This will generate a warning about CGPreflightScreenCaptureAccess and // CGRequestScreenCaptureAccess being unavailable before macOS 10.15, but // we have a guard to prevent it from being called on those earlier systems. // Unfortunately the supported way to silence this warning, using @available, // produces linker errors for __isPlatformVersionAtLeast, so we have to use // a different method. // We also ignore "tautological-pointer-compare" because when compiling with // Xcode 12.2 and later, these functions are not weakly linked and will never // be null, and therefore generate this warning. Since we are weakly linking // when compiling with earlier Xcode versions, the check for null is // necessary, and so we ignore the warning. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" #pragma clang diagnostic ignored "-Wtautological-pointer-compare" if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })] && // Double check that these weakly-linked symbols have been loaded: CGPreflightScreenCaptureAccess != nullptr && CGRequestScreenCaptureAccess != nullptr && !CGPreflightScreenCaptureAccess()) { BOOST_LOG(error) << "No screen capture permission!"sv; BOOST_LOG(error) << "Please activate it in 'System Preferences' -> 'Privacy' -> 'Screen Recording'"sv; CGRequestScreenCaptureAccess(); return nullptr; } #pragma clang diagnostic pop return std::make_unique(); } fs::path appdata() { const char *homedir; if ((homedir = getenv("HOME")) == nullptr) { homedir = getpwuid(geteuid())->pw_dir; } return fs::path { homedir } / ".config/sunshine"sv; } using ifaddr_t = util::safe_ptr; ifaddr_t get_ifaddrs() { ifaddrs *p { nullptr }; getifaddrs(&p); return ifaddr_t { p }; } std::string from_sockaddr(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); } return std::string { data }; } std::pair from_sockaddr_ex(const sockaddr *const ip_addr) { char data[INET6_ADDRSTRLEN] = {}; auto family = ip_addr->sa_family; std::uint16_t port = 0; if (family == AF_INET6) { inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data, INET6_ADDRSTRLEN); port = ((sockaddr_in6 *) ip_addr)->sin6_port; } else if (family == AF_INET) { inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data, INET_ADDRSTRLEN); port = ((sockaddr_in *) ip_addr)->sin_port; } return { port, std::string { data } }; } std::string get_mac_address(const std::string_view &address) { auto ifaddrs = get_ifaddrs(); for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) { if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) { BOOST_LOG(verbose) << "Looking for MAC of "sv << pos->ifa_name; struct ifaddrs *ifap, *ifaptr; unsigned char *ptr; std::string mac_address; if (getifaddrs(&ifap) == 0) { for (ifaptr = ifap; ifaptr != nullptr; ifaptr = (ifaptr)->ifa_next) { if (!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) { ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr); char buff[100]; snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x", *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5)); mac_address = buff; break; } } freeifaddrs(ifap); if (ifaptr != nullptr) { BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address; return mac_address; } } } } BOOST_LOG(warning) << "Unable to find MAC address for "sv << address; return "00:00:00:00:00:00"s; } bp::child run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { if (!group) { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); } else { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec); } } else { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group); } else { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group); } } } /** * @brief Open a url in the default web browser. * @param url The url to open. */ void open_url(const std::string &url) { boost::filesystem::path working_dir; std::string cmd = R"(open ")" + url + R"(")"; boost::process::environment _env = boost::this_process::environment(); std::error_code ec; auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } else { BOOST_LOG(info) << "Opened url ["sv << url << "]"sv; child.detach(); } } void adjust_thread_priority(thread_priority_e priority) { // Unimplemented } void streaming_will_start() { // Nothing to do } void streaming_will_stop() { // Nothing to do } void restart_on_exit() { char executable[2048]; uint32_t size = sizeof(executable); if (_NSGetExecutablePath(executable, &size) < 0) { BOOST_LOG(fatal) << "NSGetExecutablePath() failed: "sv << errno; return; } // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves int openmax = (int) sysconf(_SC_OPEN_MAX); for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) { close(fd); } // Re-exec ourselves with the same arguments if (execv(executable, lifetime::get_argv()) < 0) { BOOST_LOG(fatal) << "execv() failed: "sv << errno; return; } } void restart() { // Gracefully clean up and restart ourselves instead of exiting atexit(restart_on_exit); lifetime::exit_sunshine(0, true); } bool request_process_group_exit(std::uintptr_t native_handle) { if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) { BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle; return true; } else { BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno; return false; } } bool process_group_running(std::uintptr_t native_handle) { return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0; } struct sockaddr_in to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) { struct sockaddr_in saddr_v4 = {}; saddr_v4.sin_family = AF_INET; saddr_v4.sin_port = htons(port); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr)); return saddr_v4; } struct sockaddr_in6 to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) { struct sockaddr_in6 saddr_v6 = {}; saddr_v6.sin6_family = AF_INET6; saddr_v6.sin6_port = htons(port); saddr_v6.sin6_scope_id = address.scope_id(); auto addr_bytes = address.to_bytes(); memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr)); return saddr_v6; } bool send_batch(batched_send_info_t &send_info) { // Fall back to unbatched send calls return false; } bool send(send_info_t &send_info) { auto sockfd = (int) send_info.native_socket; struct msghdr msg = {}; // Convert the target address into a sockaddr struct sockaddr_in taddr_v4 = {}; struct sockaddr_in6 taddr_v6 = {}; if (send_info.target_address.is_v6()) { taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v6; msg.msg_namelen = sizeof(taddr_v6); } else { taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port); msg.msg_name = (struct sockaddr *) &taddr_v4; msg.msg_namelen = sizeof(taddr_v4); } union { char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))]; struct cmsghdr alignment; } cmbuf {}; socklen_t cmbuflen = 0; msg.msg_control = cmbuf.buf; msg.msg_controllen = sizeof(cmbuf.buf); auto pktinfo_cm = CMSG_FIRSTHDR(&msg); if (send_info.source_address.is_v6()) { struct in6_pktinfo pktInfo {}; struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0); pktInfo.ipi6_addr = saddr_v6.sin6_addr; pktInfo.ipi6_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IPV6; pktinfo_cm->cmsg_type = IPV6_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } else { struct in_pktinfo pktInfo {}; struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0); pktInfo.ipi_spec_dst = saddr_v4.sin_addr; pktInfo.ipi_ifindex = 0; cmbuflen += CMSG_SPACE(sizeof(pktInfo)); pktinfo_cm->cmsg_level = IPPROTO_IP; pktinfo_cm->cmsg_type = IP_PKTINFO; pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo)); memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo)); } struct iovec iov = {}; iov.iov_base = (void *) send_info.buffer; iov.iov_len = send_info.size; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_controllen = cmbuflen; auto bytes_sent = sendmsg(sockfd, &msg, 0); // If there's no send buffer space, wait for some to be available while (bytes_sent < 0 && errno == EAGAIN) { struct pollfd pfd; pfd.fd = sockfd; pfd.events = POLLOUT; if (poll(&pfd, 1, -1) != 1) { BOOST_LOG(warning) << "poll() failed: "sv << errno; break; } // Try to send again bytes_sent = sendmsg(sockfd, &msg, 0); } if (bytes_sent < 0) { BOOST_LOG(warning) << "sendmsg() failed: "sv << errno; return false; } return true; } // We can't track QoS state separately for each destination on this OS, // so we keep a ref count to only disable QoS options when all clients // are disconnected. static std::atomic qos_ref_count = 0; class qos_t: public deinit_t { public: qos_t(int sockfd, std::vector> options): sockfd(sockfd), options(options) { qos_ref_count++; } virtual ~qos_t() { if (--qos_ref_count == 0) { for (const auto &tuple : options) { auto reset_val = std::get<2>(tuple); if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) { BOOST_LOG(warning) << "Failed to reset option: "sv << errno; } } } } private: int sockfd; std::vector> options; }; /** * @brief Enables QoS on the given socket for traffic to the specified destination. * @param native_socket The native socket handle. * @param address The destination address for traffic sent on this socket. * @param port The destination port for traffic sent on this socket. * @param data_type The type of traffic sent on this socket. * @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic. */ std::unique_ptr enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) { int sockfd = (int) native_socket; std::vector> reset_options; // We can use SO_NET_SERVICE_TYPE to set link-layer prioritization without DSCP tagging int service_type = 0; switch (data_type) { case qos_data_type_e::video: service_type = NET_SERVICE_TYPE_VI; break; case qos_data_type_e::audio: service_type = NET_SERVICE_TYPE_VO; break; default: BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; break; } if (service_type) { if (setsockopt(sockfd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &service_type, sizeof(service_type)) == 0) { // Reset SO_NET_SERVICE_TYPE to best-effort when QoS is disabled reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_BE)); } else { BOOST_LOG(error) << "Failed to set SO_NET_SERVICE_TYPE: "sv << errno; } } if (dscp_tagging) { int level; int option; if (address.is_v6()) { level = IPPROTO_IPV6; option = IPV6_TCLASS; } else { level = IPPROTO_IP; option = IP_TOS; } // The specific DSCP values here are chosen to be consistent with Windows, // except that we use CS6 instead of CS7 for audio traffic. int dscp = 0; switch (data_type) { case qos_data_type_e::video: dscp = 40; break; case qos_data_type_e::audio: dscp = 48; break; default: BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type; break; } if (dscp) { // Shift to put the DSCP value in the correct position in the TOS field dscp <<= 2; if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) { // Reset TOS to -1 when QoS is disabled reset_options.emplace_back(std::make_tuple(level, option, -1)); } else { BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno; } } } return std::make_unique(sockfd, reset_options); } } // namespace platf namespace dyn { void * handle(const std::vector &libs) { void *handle; for (auto lib : libs) { handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL); if (handle) { return handle; } } std::stringstream ss; ss << "Couldn't find any of the following libraries: ["sv << libs.front(); std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) { ss << ", "sv << lib; }); ss << ']'; BOOST_LOG(error) << ss.str(); return nullptr; } int load(void *handle, const std::vector> &funcs, bool strict) { int err = 0; for (auto &func : funcs) { TUPLE_2D_REF(fn, name, func); *fn = (void (*)()) dlsym(handle, name); if (!*fn && strict) { BOOST_LOG(error) << "Couldn't find function: "sv << name; err = -1; } } return err; } } // namespace dyn