diff --git a/ext/installfiles/linux/zerotier-one-dns.rules b/ext/installfiles/linux/zerotier-one-dns.rules new file mode 100644 index 0000000000..10257856f9 --- /dev/null +++ b/ext/installfiles/linux/zerotier-one-dns.rules @@ -0,0 +1,13 @@ +// Allow the zerotier-one service user to configure per-interface DNS +// via systemd-resolved. Required because zerotier-one drops root +// privileges to the zerotier-one user while retaining network +// capabilities, but systemd-resolved checks polkit (not capabilities) +// for DNS configuration changes. +polkit.addRule(function(action, subject) { + if ((action.id == "org.freedesktop.resolve1.set-dns-servers" || + action.id == "org.freedesktop.resolve1.set-domains" || + action.id == "org.freedesktop.resolve1.revert") && + subject.user == "zerotier-one") { + return polkit.Result.YES; + } +}); diff --git a/make-linux.mk b/make-linux.mk index 77d3e0b1a4..403369727b 100644 --- a/make-linux.mk +++ b/make-linux.mk @@ -30,6 +30,7 @@ ifeq ($(ZT_EXTOSDEP),1) ONE_OBJS+=osdep/ExtOsdep.o override DEFS += -DZT_EXTOSDEP else + ONE_OBJS+=osdep/LinuxDNSHelper.o ONE_OBJS+=osdep/LinuxEthernetTap.o ONE_OBJS+=osdep/LinuxNetLink.o endif diff --git a/osdep/LinuxDNSHelper.cpp b/osdep/LinuxDNSHelper.cpp new file mode 100644 index 0000000000..6b82a5ab6b --- /dev/null +++ b/osdep/LinuxDNSHelper.cpp @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +#ifdef __linux__ + +#include "LinuxDNSHelper.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// Minimal PATH for subprocess execution, covering usr-merged and non-merged distros. +#define ZT_SUBPROCESS_SAFE_PATH "/usr/bin:/bin:/usr/sbin:/sbin" + +namespace ZeroTier { + +namespace { + +// Same struct layout as one.cpp — avoids pulling in libcap +struct _zt_cap_header_struct { + __u32 version; + int pid; +}; +struct _zt_cap_data_struct { + __u32 effective; + __u32 permitted; + __u32 inheritable; +}; + +static void _dropAllCapabilities() +{ + ::prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0); + _zt_cap_header_struct hdr = { _LINUX_CAPABILITY_VERSION_1, 0 }; + _zt_cap_data_struct data = { 0, 0, 0 }; + ::syscall(SYS_capset, &hdr, &data); +} + +static void _closeExtraFDs(int minFd) +{ + // close_range() syscall directly for portability with older glibc. +#ifdef SYS_close_range + if (::syscall(SYS_close_range, (unsigned int)minFd, ~0U, 0) == 0) + return; +#endif + // Fallback: iterate /proc/self/fd (always available on systemd systems) + DIR* d = ::opendir("/proc/self/fd"); + if (!d) + return; + int dirfd_val = ::dirfd(d); + struct dirent* de; + while ((de = ::readdir(d)) != nullptr) { + if (de->d_name[0] == '.') + continue; + int fd = ::atoi(de->d_name); + if (fd >= minFd && fd != dirfd_val) + ::close(fd); + } + ::closedir(d); +} + +} // anonymous namespace + +bool LinuxDNSHelper::isSystemdResolved() +{ + struct stat sb; + return (::stat("/run/systemd/resolve/stub-resolv.conf", &sb) == 0); +} + +int LinuxDNSHelper::runResolvectl(const std::vector& args) +{ + long p = (long)::fork(); + if (p > 0) { + int exitcode = -1; + ::waitpid(p, &exitcode, 0); + return WIFEXITED(exitcode) ? WEXITSTATUS(exitcode) : -1; + } + else if (p == 0) { + _dropAllCapabilities(); + _closeExtraFDs(STDOUT_FILENO); + + // resolvectl only needs D-Bus access to systemd-resolved; a minimal + // environment with a safe PATH is sufficient. + ::clearenv(); + ::setenv("PATH", ZT_SUBPROCESS_SAFE_PATH, 1); + + std::vector argv; + argv.push_back("resolvectl"); + for (size_t i = 0; i < args.size(); ++i) { + argv.push_back(args[i].c_str()); + } + argv.push_back(nullptr); + ::execvp("resolvectl", const_cast(argv.data())); + ::_exit(-1); + } + return -1; +} + +void LinuxDNSHelper::setDNS(const char* interfaceName, const char* domain, const std::vector& servers) +{ + if (! isSystemdResolved()) { + fprintf( + stderr, + "WARNING: systemd-resolved not detected. DNS configuration for ZeroTier networks requires systemd-resolved or manual configuration. " + "See https://github.com/zerotier/ZeroTierOne/issues/2492 for details" ZT_EOL_S); + return; + } + + if (! interfaceName || ! interfaceName[0] || servers.empty()) { + return; + } + + // resolvectl dns ... + { + std::vector args; + args.push_back("dns"); + args.push_back(interfaceName); + for (size_t i = 0; i < servers.size(); ++i) { + char buf[64]; + args.push_back(servers[i].toIpString(buf)); + } + int rc = runResolvectl(args); + if (rc != 0) { + fprintf(stderr, "WARNING: resolvectl dns failed (exit %d) for interface %s" ZT_EOL_S, rc, interfaceName); + return; + } + } + + // resolvectl domain ~ + if (domain && domain[0]) { + std::vector args; + args.push_back("domain"); + args.push_back(interfaceName); + args.push_back(std::string("~") + domain); + int rc = runResolvectl(args); + if (rc != 0) { + fprintf(stderr, "WARNING: resolvectl domain failed (exit %d) for interface %s" ZT_EOL_S, rc, interfaceName); + } + } +} + +void LinuxDNSHelper::removeDNS(const char* interfaceName) +{ + if (! isSystemdResolved()) { + return; + } + + if (! interfaceName || ! interfaceName[0]) { + return; + } + + std::vector args; + args.push_back("revert"); + args.push_back(interfaceName); + int rc = runResolvectl(args); + if (rc != 0) { + fprintf(stderr, "WARNING: resolvectl revert failed (exit %d) for interface %s" ZT_EOL_S, rc, interfaceName); + } +} + +} // namespace ZeroTier + +#endif diff --git a/osdep/LinuxDNSHelper.hpp b/osdep/LinuxDNSHelper.hpp new file mode 100644 index 0000000000..6b67ea72bc --- /dev/null +++ b/osdep/LinuxDNSHelper.hpp @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c) ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +#ifndef LINUX_DNS_HELPER_H_ +#define LINUX_DNS_HELPER_H_ + +#include "../node/InetAddress.hpp" + +#include +#include + +namespace ZeroTier { + +class LinuxDNSHelper { + public: + static void setDNS(const char* interfaceName, const char* domain, const std::vector& servers); + static void removeDNS(const char* interfaceName); + + private: + static bool isSystemdResolved(); + static int runResolvectl(const std::vector& args); +}; + +} // namespace ZeroTier + +#endif diff --git a/osdep/LinuxEthernetTap.cpp b/osdep/LinuxEthernetTap.cpp index efaa729595..adc2db292f 100644 --- a/osdep/LinuxEthernetTap.cpp +++ b/osdep/LinuxEthernetTap.cpp @@ -17,6 +17,7 @@ #include "../node/Dictionary.hpp" #include "../node/Mutex.hpp" #include "../node/Utils.hpp" +#include "LinuxDNSHelper.hpp" #include "LinuxEthernetTap.hpp" #include "LinuxNetLink.hpp" #include "OSUtils.hpp" @@ -358,6 +359,7 @@ LinuxEthernetTap::LinuxEthernetTap( LinuxEthernetTap::~LinuxEthernetTap() { _run = false; + LinuxDNSHelper::removeDNS(_dev.c_str()); (void)::write(_shutdownSignalPipe[1], "\0", 1); ::close(_fd); ::close(_shutdownSignalPipe[0]); @@ -589,6 +591,11 @@ void LinuxEthernetTap::setMtu(unsigned int mtu) } } +void LinuxEthernetTap::setDns(const char* domain, const std::vector& servers) +{ + LinuxDNSHelper::setDNS(_dev.c_str(), domain, servers); +} + } // namespace ZeroTier #endif // __LINUX__ diff --git a/osdep/LinuxEthernetTap.hpp b/osdep/LinuxEthernetTap.hpp index 6d249698f9..f92e6b2e42 100644 --- a/osdep/LinuxEthernetTap.hpp +++ b/osdep/LinuxEthernetTap.hpp @@ -52,10 +52,7 @@ class LinuxEthernetTap : public EthernetTap { virtual void setFriendlyName(const char* friendlyName); virtual void scanMulticastGroups(std::vector& added, std::vector& removed); virtual void setMtu(unsigned int mtu); - virtual void setDns(const char* domain, const std::vector& servers) - { - fprintf(stderr, "WARNING: ignoring call to LinuxEthernetTap::setDns on Linux. This is not implemented yet. See https://github.com/zerotier/ZeroTierOne/issues/2492 for details" ZT_EOL_S); - } + virtual void setDns(const char* domain, const std::vector& servers); private: void (*_handler)(void*, void*, uint64_t, const MAC&, const MAC&, unsigned int, unsigned int, const void*, unsigned int); diff --git a/service/OneService.cpp b/service/OneService.cpp index 227b575f7e..2a65106c09 100644 --- a/service/OneService.cpp +++ b/service/OneService.cpp @@ -102,6 +102,8 @@ namespace sdkresource = opentelemetry::v1::sdk::resource; #elif defined(__WINDOWS__) #include "../osdep/WinDNSHelper.hpp" #include "../osdep/WinFWHelper.hpp" +#elif defined(__linux__) +#include "../osdep/LinuxDNSHelper.hpp" #endif #ifdef ZT_USE_SYSTEM_HTTP_PARSER @@ -3207,6 +3209,10 @@ class OneServiceImpl : public OneService { MacDNSHelper::removeDNS(n.config().nwid); #elif defined(__WINDOWS__) WinDNSHelper::removeDNS(n.config().nwid); +#elif defined(__linux__) + if (n.tap()) { + LinuxDNSHelper::removeDNS(n.tap()->deviceName().c_str()); + } #endif } } diff --git a/zerotier-one.spec b/zerotier-one.spec index be57e77236..648b2177bc 100644 --- a/zerotier-one.spec +++ b/zerotier-one.spec @@ -118,6 +118,8 @@ make ZT_USE_MINIUPNPC=1 %{?_smp_mflags} ZT_OFFICIAL=1 ZT_NONFREE=1 one make install DESTDIR=$RPM_BUILD_ROOT mkdir -p $RPM_BUILD_ROOT%{_unitdir} cp %{getenv:PWD}/debian/zerotier-one.service $RPM_BUILD_ROOT%{_unitdir}/%{name}.service +mkdir -p $RPM_BUILD_ROOT%{_sysconfdir}/polkit-1/rules.d +cp %{getenv:PWD}/ext/installfiles/linux/zerotier-one-dns.rules $RPM_BUILD_ROOT%{_sysconfdir}/polkit-1/rules.d/49-zerotier-one-dns.rules %else rm -rf $RPM_BUILD_ROOT pushd %{getenv:PWD} @@ -137,6 +139,7 @@ chmod 0755 $RPM_BUILD_ROOT/etc/init.d/zerotier-one /etc/init.d/zerotier-one %else %{_unitdir}/%{name}.service +%{_sysconfdir}/polkit-1/rules.d/49-zerotier-one-dns.rules %endif %post