OmniOS 와 Zerotier One 에 대해
OmniOS 는 Solaris 기반의 오픈소스 운영체제다. Oracle Solaris에서 파생된 illumos 프로젝트를 기반으로 하며, 서버 환경에서의 안정성과 성능에 중점을 둔 Unix 계열 OS다. ZFS 파일시스템과 DTrace 등 Solaris의 강력한 기능들을 그대로 제공하면서도 완전히 오픈소스로 유지되고 있어, 엔터프라이즈 환경에서 높은 평가를 받고 있다.
ZeroTier One 은 인터넷을 통해 전 세계 어디서나 장치들을 하나의 가상 로컬 영역 네트워크(VLAN)로 연결해 주는 소프트웨어 정의 네트워크(SDN) 솔루션이다. 복잡한 포트 포워딩이나 기존 VPN 의 번거로운 설정 없이도 P2P 기반의 엔드투엔드 암호화를 통한 안전한 통신을 제공한다 – 멀티 클라우드, 온프레미스, IoT 기기 등 물리적 위치에 구애받지 않는 유연한 네트워크 구성이 그것이다. 특히 다양한 운영체제를 지원하여 하이브리드 인프라 환경에 최적화되어 있다.
일반적으로 ZeroTier One 은 Linux, FreeBSD, macOS, Windows 위주로 사용되지만, OmniOS 에서도 약간의 플랫폼 보정을 거치면 충분히 빌드하고 서비스화할 수 있다. 특히 OmniOS/illumos 계열은 etherstub 위에 vnic 를 만들 수 있고, libdlpi 는 DLPI 링크를 다루는 사용자 공간 인터페이스를 제공하므로, 전형적인 tuntap 경로 대신 OmniOS 네이티브 네트워크 가상화 방식으로 우회하는 접근이 잘 맞는다. 내 경우에도 tuntap 기반 경로는 실패했지만, etherstub + vnic + libdlpi 방식으로는 정상 동작을 확인했다.
이 글은 2025년 5월 5일 릴리즈된 OmniOS r151054 LTS Release 를 설치한 뒤, 2026년 3월 14일 기준으로 pkg update 로 패키지를 모두 최신 상태까지 올린 서버에서 실제로 성공한 설치 절차를 기준으로 작성했다. 즉, “이론상 될 것 같은 방법”이 아니라, 직접 검증 완료된 경로를 블로그 글 형식으로 다시 정리한 것이다.
공식적으로 2026년 3월 14일 기준 최신 Zerotier One 정식 릴리즈는 1.16.0이다. 다만 이 글의 설치법은 릴리즈 tarball 중심이 아니라 clone + patch + SMF 등록 방식이기 때문에, “최신 릴리즈 소개” 와는 목적이 조금 다르다. 즉, 이 글은 “내 검증 시점에는 이 상태에서 patch가 잘 적용되고 빌드가 됐다”는 실증 기록이다.
요구 사항
- OmniOS r151054 LTS Release
- root 권한
- 인터넷 연결
OS 정보
root@omnios-lts:~# uname -a
SunOS omnios-lts 5.11 omnios-r151054-345feddfd2 i86pc i386 i86pc
root@omnios-lts:~# cat /etc/os-release
NAME="OmniOS"
PRETTY_NAME="OmniOS Community Edition v11 r151054am"
CPE_NAME="cpe:/o:omniosce:omnios:11:151054:39"
ID=omnios
VERSION=r151054am
VERSION_ID=r151054am
BUILD_ID=151054.39.2026.01.29
HOME_URL="https://omnios.org/"
SUPPORT_URL="https://omnios.org/"
BUG_REPORT_URL="https://github.com/omniosorg/omnios-build/issues/new"1. 필수 패키지 설치
pkg install git gnu-make gcc14이 단계는 빌드 체인의 최소 구성이다. git 은 upstream 소스를 직접 내려받기 위해 필요하고, gnu-make 는 ZeroTier One 의 BSD 계열 빌드 규칙을 타기 위해 필요하다. 공식 build.md 도 FreeBSD/OpenBSD 계열에서는 gmake 를 사용한다고 설명한다.
컴파일러를 gcc14로 고정한 이유도 분명하다. 뒤에서 적용할 patch를 보면 SunOS 경로에서는 기본 BSD/Linux 빌드가 쓰는 -fPIE, -pie, strip --strip-all 조합을 그대로 쓰지 않고, SunOS에 맞는 최소한의 플래그만 유지하도록 바꾼다. 즉, 이 글은 “OmniOS에 native한 빌드 환경”을 전제로 하지 않고, GNU 툴체인을 앞세운 실용적인 재현 경로를 택한 것이다.
2. ZeroTier One 소스 코드 clone
git clone https://github.com/zerotier/ZeroTierOne.git /opt/ZeroTierOne
cd /opt/ZeroTierOne
git checkout --force d9a7f62a5ca04f832d1025bcc7c48f9e8d65e3a6
git reset --hard d9a7f62a5ca04f832d1025bcc7c48f9e8d65e3a6
git clean -fdx최신 HEAD를 따라가는 방식이 아니라, 실제로 OmniOS r151054 LTS Release 에서 동작을 검증한 커밋에 고정하는 방식이다.
3. OmniOS patch 다운로드 및 적용
wget https://www.ourdare.com/resources/omnios-zerotier-one.patch -O /opt/ZeroTierOne/omnios-zerotier-one.patch
patch -p1 < omnios-zerotier-one.patchdiff --git a/make-bsd.mk b/make-bsd.mk
index cf4c22c..67a811e 100644
--- a/make-bsd.mk
+++ b/make-bsd.mk
@@ -1,5 +1,7 @@
# This requires GNU make, which is typically "gmake" on BSD systems
+HOST_UNAME_S=$(shell uname -s)
+
INCLUDES=-isystem ext -Iext/prometheus-cpp-lite-1.0/core/include -Iext/prometheus-cpp-lite-1.0/simpleapi/include -Iext/opentelemetry-cpp-api-only/include
DEFS=
LIBS=
@@ -39,6 +41,11 @@ ifeq ($(OSTYPE),FreeBSD)
endif
endif
+ifeq ($(HOST_UNAME_S),SunOS)
+ override DEFS+=-D__UNIX_LIKE__
+ LIBS+=-lsocket -lnsl -ldlpi
+endif
+
# Build with address sanitization library for advanced debugging (clang)
ifeq ($(ZT_SANITIZE),1)
SANFLAGS+=-fsanitize=address -DASAN_OPTIONS=symbolize=1
@@ -54,9 +61,14 @@ ifeq ($(ZT_DEBUG),1)
node/Salsa20.o node/SHA512.o node/C25519.o node/Poly1305.o: CFLAGS = -Wall -O2 -g -pthread $(INCLUDES) $(DEFS)
else
CFLAGS?=-O3 -fstack-protector
- CFLAGS+=-Wall -fPIE -fvisibility=hidden -fstack-protector -pthread $(INCLUDES) -DNDEBUG $(DEFS)
- LDFLAGS+=-pie -Wl,-z,relro,-z,now
- STRIP=strip --strip-all
+ CFLAGS+=-Wall -fvisibility=hidden -fstack-protector -pthread $(INCLUDES) -DNDEBUG $(DEFS)
+ ifneq ($(HOST_UNAME_S),SunOS)
+ CFLAGS+=-fPIE
+ LDFLAGS+=-pie -Wl,-z,relro,-z,now
+ STRIP=strip --strip-all
+ else
+ STRIP=strip
+ endif
endif
ifeq ($(ZT_TRACE),1)
diff --git a/node/Constants.hpp b/node/Constants.hpp
index 6baf8c7..d421400 100644
--- a/node/Constants.hpp
+++ b/node/Constants.hpp
@@ -86,6 +86,12 @@
#endif
#endif
+#if defined(__sun) || defined(__sun__)
+#ifndef __UNIX_LIKE__
+#define __UNIX_LIKE__
+#endif
+#endif
+
#if defined(_WIN32) || defined(_WIN64)
#ifdef ZT_SSO_SUPPORTED
#define ZT_SSO_ENABLED 1
diff --git a/one.cpp b/one.cpp
index a066f4e..8cffbbf 100644
--- a/one.cpp
+++ b/one.cpp
@@ -549,9 +549,9 @@ static int cli(int argc, char** argv)
nlohmann::json& p = j[k];
bool isBonded = p["isBonded"];
if (isBonded) {
- int8_t bondingPolicyCode = p["bondingPolicyCode"];
- int8_t numAliveLinks = p["numAliveLinks"];
- int8_t numTotalLinks = p["numTotalLinks"];
+ int bondingPolicyCode = OSUtils::jsonInt(p["bondingPolicyCode"], 0);
+ int numAliveLinks = OSUtils::jsonInt(p["numAliveLinks"], 0);
+ int numTotalLinks = OSUtils::jsonInt(p["numTotalLinks"], 0);
bFoundBond = true;
std::string policyStr = "none";
if (bondingPolicyCode >= ZT_BOND_POLICY_NONE && bondingPolicyCode <= ZT_BOND_POLICY_BALANCE_AWARE) {
@@ -734,9 +734,9 @@ static int cli(int argc, char** argv)
nlohmann::json& p = j[k];
bool isBonded = p["isBonded"];
if (isBonded) {
- int8_t bondingPolicyCode = p["bondingPolicyCode"];
- int8_t numAliveLinks = p["numAliveLinks"];
- int8_t numTotalLinks = p["numTotalLinks"];
+ int bondingPolicyCode = OSUtils::jsonInt(p["bondingPolicyCode"], 0);
+ int numAliveLinks = OSUtils::jsonInt(p["numAliveLinks"], 0);
+ int numTotalLinks = OSUtils::jsonInt(p["numTotalLinks"], 0);
bFoundBond = true;
std::string policyStr = "none";
if (bondingPolicyCode >= ZT_BOND_POLICY_NONE && bondingPolicyCode <= ZT_BOND_POLICY_BALANCE_AWARE) {
diff --git a/osdep/BSDEthernetTap.cpp b/osdep/BSDEthernetTap.cpp
index 82ac07d..e899af6 100644
--- a/osdep/BSDEthernetTap.cpp
+++ b/osdep/BSDEthernetTap.cpp
@@ -22,10 +22,14 @@
#include <net/if.h>
#include <net/if_arp.h>
#include <net/if_dl.h>
+#if !defined(__sun)
#include <net/if_media.h>
+#endif
#include <net/route.h>
#include <netinet/in.h>
+#if !defined(__sun)
#include <pthread_np.h>
+#endif
#include <sched.h>
#include <set>
#include <signal.h>
@@ -34,7 +38,9 @@
#include <stdlib.h>
#include <string.h>
#include <string>
+#if !defined(__sun)
#include <sys/cdefs.h>
+#endif
#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/select.h>
@@ -45,15 +51,165 @@
#include <sys/wait.h>
#include <unistd.h>
#include <utility>
+#if defined(__sun)
+#include <stropts.h>
+#include <sys/dlpi.h>
+#include <sys/sockio.h>
+#endif
#define ZT_BASE32_CHARS "0123456789abcdefghijklmnopqrstuv"
#define ZT_TAP_BUF_SIZE (1024 * 16)
-
// ff:ff:ff:ff:ff:ff with no ADI
static const ZeroTier::MulticastGroup _blindWildcardMulticastGroup(ZeroTier::MAC(0xff), 0);
namespace ZeroTier {
+#ifdef __sun
+static bool _sunIfMatches(const char* expected, const char* actual)
+{
+ if ((! expected) || (! actual))
+ return false;
+ const size_t expectedLen = strlen(expected);
+ return (strncmp(expected, actual, expectedLen) == 0) && ((actual[expectedLen] == 0) || (actual[expectedLen] == ':'));
+}
+
+static int _sunRun(const std::initializer_list<const char*>& argv)
+{
+ std::vector<char*> args;
+ args.reserve(argv.size() + 1);
+ if (argv.size() == 0)
+ return -1;
+
+ auto it = argv.begin();
+ const char* execPath = *it++;
+ args.push_back(const_cast<char*>(execPath));
+ for (; it != argv.end(); ++it) {
+ if ((args.size() == 1) && (! strcmp(*it, execPath)))
+ continue;
+ args.push_back(const_cast<char*>(*it));
+ }
+ args.push_back((char*)0);
+
+ long cpid = (long)::vfork();
+ if (cpid == 0) {
+ int nullFd = ::open("/dev/null", O_RDWR);
+ if (nullFd >= 0) {
+ ::dup2(nullFd, STDOUT_FILENO);
+ ::dup2(nullFd, STDERR_FILENO);
+ if (nullFd > STDERR_FILENO)
+ ::close(nullFd);
+ }
+ ::execv(args[0], args.data());
+ ::_exit(127);
+ }
+ if (cpid < 0)
+ return -1;
+
+ int exitcode = -1;
+ ::waitpid(cpid, &exitcode, 0);
+ return exitcode;
+}
+
+static std::string _sunMacString(const MAC& mac)
+{
+ char buf[18];
+ OSUtils::ztsnprintf(buf, sizeof(buf), "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x", (int)mac[0], (int)mac[1], (int)mac[2], (int)mac[3], (int)mac[4], (int)mac[5]);
+ return std::string(buf);
+}
+
+static MAC _sunBackendMac(const MAC& mac)
+{
+ MAC backend(mac);
+ uint8_t b[6];
+ uint8_t original[6];
+ backend.copyTo(b, 6);
+ mac.copyTo(original, 6);
+ b[0] |= 0x02;
+ b[0] &= 0xfe;
+ b[5] ^= 0x01;
+ if (! memcmp(b, original, 6))
+ b[4] ^= 0x80;
+ return MAC(b, 6);
+}
+
+static std::string _sunMakeName(const char* prefix, uint64_t nwid)
+{
+ std::string name(prefix);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 60) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 55) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 50) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 45) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 40) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 35) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 30) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 25) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 20) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 15) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 10) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)((nwid >> 5) & 0x1f)]);
+ name.push_back(ZT_BASE32_CHARS[(unsigned long)(nwid & 0x1f)]);
+ return name;
+}
+
+static bool _sunOpenRawHandle(const std::string& dev, bool promiscuous, dlpi_handle_t& dh, int& fd)
+{
+ dh = (dlpi_handle_t)0;
+ fd = -1;
+ int rc = ::dlpi_open(dev.c_str(), &dh, DLPI_RAW);
+ if (rc != DLPI_SUCCESS)
+ return false;
+ rc = ::dlpi_bind(dh, DLPI_ANY_SAP, (uint_t*)0);
+ if (rc != DLPI_SUCCESS)
+ return false;
+ if (promiscuous) {
+ rc = ::dlpi_promiscon(dh, DL_PROMISC_PHYS);
+ if (rc != DLPI_SUCCESS)
+ return false;
+ rc = ::dlpi_promiscon(dh, DL_PROMISC_SAP);
+ if (rc != DLPI_SUCCESS)
+ return false;
+ rc = ::dlpi_promiscon(dh, DL_PROMISC_MULTI);
+ if (rc != DLPI_SUCCESS)
+ return false;
+ }
+ fd = ::dlpi_fd(dh);
+ return (fd >= 0);
+}
+
+static bool _sunCreateDlpiStack(const std::string& stubDev, const std::string& visDev, const std::string& backDev, const MAC& visMac, const MAC& backMac, unsigned int mtu, dlpi_handle_t& rxDh, dlpi_handle_t& txDh, int& rxFd)
+{
+ std::string mtuStr(std::to_string(mtu));
+ std::string visMacStr(_sunMacString(visMac));
+ std::string backMacStr(_sunMacString(backMac));
+ rxDh = (dlpi_handle_t)0;
+ txDh = (dlpi_handle_t)0;
+ rxFd = -1;
+ int txFd = -1;
+
+ (void)_sunRun({"/usr/sbin/ifconfig", "/usr/sbin/ifconfig", visDev.c_str(), "unplumb"});
+ (void)_sunRun({"/usr/sbin/ifconfig", "/usr/sbin/ifconfig", visDev.c_str(), "inet6", "unplumb"});
+ (void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "delete-vnic", backDev.c_str()});
+ (void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "delete-vnic", visDev.c_str()});
+ (void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "delete-etherstub", stubDev.c_str()});
+
+ if (_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "create-etherstub", stubDev.c_str()}) != 0)
+ return false;
+ if (_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "create-vnic", "-t", "-m", visMacStr.c_str(), "-l", stubDev.c_str(), visDev.c_str()}) != 0)
+ return false;
+ if (_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "create-vnic", "-t", "-m", backMacStr.c_str(), "-l", stubDev.c_str(), backDev.c_str()}) != 0)
+ return false;
+ if (_sunRun({"/usr/sbin/ifconfig", "/usr/sbin/ifconfig", visDev.c_str(), "plumb", "mtu", mtuStr.c_str(), "up"}) != 0)
+ return false;
+
+ if (! _sunOpenRawHandle(visDev, true, rxDh, rxFd))
+ return false;
+ if (! _sunOpenRawHandle(backDev, false, txDh, txFd))
+ return false;
+ rxFd = ::dlpi_fd(rxDh);
+ return (rxFd >= 0);
+}
+#endif
+
BSDEthernetTap::BSDEthernetTap(
const char* homePath,
unsigned int concurrency,
@@ -72,7 +228,18 @@ BSDEthernetTap::BSDEthernetTap(
, _nwid(nwid)
, _mtu(mtu)
, _metric(metric)
- , _fd(0)
+ , _fd(-1)
+ , _ipFd(-1)
+ , _ipMuxId(-1)
+ , _ppa(-1)
+#ifdef __sun
+ , _sunDlpi((dlpi_handle_t)0)
+ , _sunTxDlpi((dlpi_handle_t)0)
+ , _sunBackendDev()
+ , _sunStubDev()
+ , _sunVisibleMac(mac)
+#endif
+ , _destroyPersistent(false)
, _enabled(true)
, _lastIfAddrsUpdate(0)
{
@@ -81,7 +248,15 @@ BSDEthernetTap::BSDEthernetTap(
Mutex::Lock _gl(globalTapCreateLock);
-#ifdef __FreeBSD__
+#ifdef __sun
+ (void)tmpdevname;
+ (void)devpath;
+ _dev = _sunMakeName("zt", nwid);
+ _sunBackendDev = _sunMakeName("zb", nwid);
+ _sunStubDev = _sunMakeName("zs", nwid);
+ if (! _sunCreateDlpiStack(_sunStubDev, _dev, _sunBackendDev, mac, _sunBackendMac(mac), _mtu, _sunDlpi, _sunTxDlpi, _fd))
+ _fd = -1;
+#elif defined(__FreeBSD__)
/* FreeBSD allows long interface names and interface renaming */
_dev = "zt";
@@ -163,7 +338,7 @@ BSDEthernetTap::BSDEthernetTap(
}
#endif
- if (_fd <= 0)
+ if (_fd < 0)
throw std::runtime_error("unable to open TAP device or no more devices available");
if (fcntl(_fd, F_SETFL, fcntl(_fd, F_GETFL) & ~O_NONBLOCK) == -1) {
@@ -175,6 +350,7 @@ BSDEthernetTap::BSDEthernetTap(
OSUtils::ztsnprintf(ethaddr, sizeof(ethaddr), "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x", (int)mac[0], (int)mac[1], (int)mac[2], (int)mac[3], (int)mac[4], (int)mac[5]);
OSUtils::ztsnprintf(mtustr, sizeof(mtustr), "%u", _mtu);
OSUtils::ztsnprintf(metstr, sizeof(metstr), "%u", _metric);
+#ifndef __sun
long cpid = (long)vfork();
if (cpid == 0) {
#ifdef ZT_TRACE
@@ -191,6 +367,7 @@ BSDEthernetTap::BSDEthernetTap(
throw std::runtime_error("ifconfig failure setting link-layer address and activating tap interface");
}
}
+#endif
// Set close-on-exec so that devices cannot persist if we fork/exec for update
fcntl(_fd, F_SETFD, fcntl(_fd, F_GETFD) | FD_CLOEXEC);
@@ -203,9 +380,13 @@ BSDEthernetTap::BSDEthernetTap(
BSDEthernetTap::~BSDEthernetTap()
{
::write(_shutdownSignalPipe[1], "\0", 1); // causes thread to exit
- ::close(_fd);
+#ifndef __sun
+ if (_fd >= 0)
+ ::close(_fd);
+#endif
::close(_shutdownSignalPipe[0]);
::close(_shutdownSignalPipe[1]);
+#ifndef __sun
long cpid = (long)vfork();
if (cpid == 0) {
#ifdef ZT_TRACE
@@ -218,7 +399,24 @@ BSDEthernetTap::~BSDEthernetTap()
int exitcode = -1;
::waitpid(cpid, &exitcode, 0);
}
+#endif
Thread::join(_thread);
+#ifdef __sun
+ if (_sunDlpi) {
+ ::dlpi_close(_sunDlpi);
+ _sunDlpi = (dlpi_handle_t)0;
+ }
+ if (_sunTxDlpi) {
+ ::dlpi_close(_sunTxDlpi);
+ _sunTxDlpi = (dlpi_handle_t)0;
+ }
+ _fd = -1;
+ (void)_sunRun({"/usr/sbin/ifconfig", "/usr/sbin/ifconfig", _dev.c_str(), "unplumb"});
+ (void)_sunRun({"/usr/sbin/ifconfig", "/usr/sbin/ifconfig", _dev.c_str(), "inet6", "unplumb"});
+ (void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "delete-vnic", _sunBackendDev.c_str()});
+ (void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "delete-vnic", _dev.c_str()});
+ (void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "delete-etherstub", _sunStubDev.c_str()});
+#endif
for (std::thread& t : _rxThreads) {
t.join();
}
@@ -239,10 +437,21 @@ static bool ___removeIp(const std::string& _dev, const InetAddress& ip)
long cpid = (long)vfork();
if (cpid == 0) {
char ipbuf[64];
+#ifdef __sun
+ execl(
+ "/sbin/ifconfig",
+ "/sbin/ifconfig",
+ _dev.c_str(),
+ ip.isV4() ? "removeif" : "inet6",
+ ip.isV4() ? ip.toIpString(ipbuf) : "removeif",
+ ip.isV4() ? (const char*)0 : ip.toIpString(ipbuf),
+ (const char*)0);
+#else
#ifdef ZT_TRACE
fprintf(stderr, "DEBUG: ifconfig %s inet %s -alias" ZT_EOL_S, _dev.c_str(), ip.toIpString(ipbuf));
#endif
execl("/sbin/ifconfig", "/sbin/ifconfig", _dev.c_str(), "inet", ip.toIpString(ipbuf), "-alias", (const char*)0);
+#endif
_exit(-1);
}
else if (cpid > 0) {
@@ -273,10 +482,39 @@ bool BSDEthernetTap::addIp(const InetAddress& ip)
long cpid = (long)vfork();
if (cpid == 0) {
char tmp[128];
+#ifdef __sun
+ if (ip.isV4()) {
+ long plumbPid = (long)vfork();
+ if (plumbPid == 0) {
+ ::execl("/sbin/ifconfig", "/sbin/ifconfig", _dev.c_str(), "plumb", "up", (const char*)0);
+ ::_exit(-1);
+ }
+ else if (plumbPid > 0) {
+ int plumbExitCode = -1;
+ ::waitpid(plumbPid, &plumbExitCode, 0);
+ (void)plumbExitCode;
+ }
+ ::execl("/sbin/ifconfig", "/sbin/ifconfig", _dev.c_str(), "addif", ip.toString(tmp), "up", (const char*)0);
+ }
+ else {
+ long plumbPid = (long)vfork();
+ if (plumbPid == 0) {
+ ::execl("/sbin/ifconfig", "/sbin/ifconfig", _dev.c_str(), "inet6", "plumb", "up", (const char*)0);
+ ::_exit(-1);
+ }
+ else if (plumbPid > 0) {
+ int plumbExitCode = -1;
+ ::waitpid(plumbPid, &plumbExitCode, 0);
+ (void)plumbExitCode;
+ }
+ ::execl("/sbin/ifconfig", "/sbin/ifconfig", _dev.c_str(), "inet6", "addif", ip.toString(tmp), "up", (const char*)0);
+ }
+#else
#ifdef ZT_TRACE
fprintf(stderr, "DEBUG: ifconfig %s %s %s alias" ZT_EOL_S, _dev.c_str(), ip.isV4() ? "inet" : "inet6", ip.toString(tmp));
#endif
::execl("/sbin/ifconfig", "/sbin/ifconfig", _dev.c_str(), ip.isV4() ? "inet" : "inet6", ip.toString(tmp), "alias", (const char*)0);
+#endif
::_exit(-1);
}
else if (cpid > 0) {
@@ -316,7 +554,13 @@ std::vector<InetAddress> BSDEthernetTap::ips() const
struct ifaddrs* p = ifa;
while (p) {
- if ((! strcmp(p->ifa_name, _dev.c_str())) && (p->ifa_addr) && (p->ifa_netmask) && (p->ifa_addr->sa_family == p->ifa_netmask->sa_family)) {
+ if (
+#ifdef __sun
+ _sunIfMatches(_dev.c_str(), p->ifa_name) &&
+#else
+ (! strcmp(p->ifa_name, _dev.c_str())) &&
+#endif
+ (p->ifa_addr) && (p->ifa_netmask) && (p->ifa_addr->sa_family == p->ifa_netmask->sa_family)) {
switch (p->ifa_addr->sa_family) {
case AF_INET: {
struct sockaddr_in* sin = (struct sockaddr_in*)p->ifa_addr;
@@ -355,7 +599,13 @@ void BSDEthernetTap::put(const MAC& from, const MAC& to, unsigned int etherType,
*((uint16_t*)(putBuf + 12)) = htons((uint16_t)etherType);
memcpy(putBuf + 14, data, len);
len += 14;
+#ifdef __sun
+ if (_sunTxDlpi) {
+ (void)::dlpi_send(_sunTxDlpi, (const void*)0, 0, putBuf, len, (const dlpi_sendinfo_t*)0);
+ }
+#else
::write(_fd, putBuf, len);
+#endif
}
}
@@ -372,7 +622,7 @@ void BSDEthernetTap::scanMulticastGroups(std::vector<MulticastGroup>& added, std
{
std::vector<MulticastGroup> newGroups;
-#ifndef __OpenBSD__
+#if !defined(__OpenBSD__) && !defined(__sun)
struct ifmaddrs* ifmap = (struct ifmaddrs*)0;
if (! getifmaddrs(&ifmap)) {
struct ifmaddrs* p = ifmap;
@@ -387,7 +637,7 @@ void BSDEthernetTap::scanMulticastGroups(std::vector<MulticastGroup>& added, std
}
freeifmaddrs(ifmap);
}
-#endif // __OpenBSD__
+#endif // !__OpenBSD__ && !__sun
std::vector<InetAddress> allIps(ips());
for (std::vector<InetAddress>::iterator ip(allIps.begin()); ip != allIps.end(); ++ip)
@@ -435,11 +685,11 @@ void BSDEthernetTap::threadMain() throw()
// constructing itself.
Thread::sleep(500);
-#ifndef __OpenBSD__
bool pinning = _pinning;
for (unsigned int i = 0; i < _concurrency; ++i) {
_rxThreads.push_back(std::thread([this, i, pinning] {
+#if !defined(__OpenBSD__) && !defined(__sun)
if (pinning) {
int pinCore = i % _concurrency;
fprintf(stderr, "Pinning thread %d to core %d\n", i, pinCore);
@@ -454,7 +704,9 @@ void BSDEthernetTap::threadMain() throw()
exit(1);
}
}
-#endif // __OpenBSD__
+#else
+ (void)pinning;
+#endif
uint8_t b[ZT_TAP_BUF_SIZE];
MAC to, from;
@@ -476,6 +728,19 @@ void BSDEthernetTap::threadMain() throw()
break;
if (FD_ISSET(_fd, &readfds)) {
+#ifdef __sun
+ size_t msglen = sizeof(b);
+ int dlpiRc = _sunDlpi ? ::dlpi_recv(_sunDlpi, (void*)0, (size_t*)0, b, &msglen, 0, (dlpi_recvinfo_t*)0) : DLPI_FAILURE;
+ if (dlpiRc == DLPI_SUCCESS) {
+ n = (int)msglen;
+ }
+ else if (dlpiRc == DLPI_ETIMEDOUT) {
+ continue;
+ }
+ else {
+ break;
+ }
+#else
n = (int)::read(_fd, b + r, sizeof(b) - r);
if (n < 0) {
if ((errno != EINTR) && (errno != ETIMEDOUT))
@@ -500,12 +765,27 @@ void BSDEthernetTap::threadMain() throw()
r = 0;
}
}
+#endif
+#ifdef __sun
+ if (n > 14) {
+ uint8_t visibleMac[6];
+ _sunVisibleMac.copyTo(visibleMac, 6);
+ if (memcmp(b + 6, visibleMac, 6) != 0)
+ continue;
+ if (n > ((int)_mtu + 14))
+ n = _mtu + 14;
+ if (_enabled) {
+ to.setTo(b, 6);
+ from.setTo(b + 6, 6);
+ unsigned int etherType = ntohs(((const uint16_t*)b)[6]);
+ _handler(_arg, (void*)0, _nwid, from, to, etherType, 0, (const void*)(b + 14), n - 14);
+ }
+ }
+#endif
}
}
-#ifndef __OpenBSD__
}));
}
-#endif // __OpenBSD__
}
} // namespace ZeroTier
diff --git a/osdep/BSDEthernetTap.hpp b/osdep/BSDEthernetTap.hpp
index 190f3a0..d5ed8ee 100644
--- a/osdep/BSDEthernetTap.hpp
+++ b/osdep/BSDEthernetTap.hpp
@@ -21,6 +21,9 @@
#include <string>
#include <thread>
#include <vector>
+#ifdef __sun
+#include <libdlpi.h>
+#endif
namespace ZeroTier {
@@ -68,7 +71,18 @@ class BSDEthernetTap : public EthernetTap {
unsigned int _mtu;
unsigned int _metric;
int _fd;
+ int _ipFd;
+ int _ipMuxId;
+ int _ppa;
+#ifdef __sun
+ dlpi_handle_t _sunDlpi;
+ dlpi_handle_t _sunTxDlpi;
+ std::string _sunBackendDev;
+ std::string _sunStubDev;
+ MAC _sunVisibleMac;
+#endif
int _shutdownSignalPipe[2];
+ bool _destroyPersistent;
volatile bool _enabled;
mutable std::vector<InetAddress> _ifaddrs;
mutable uint64_t _lastIfAddrsUpdate;
diff --git a/osdep/EthernetTap.cpp b/osdep/EthernetTap.cpp
index 1d35e01..cf52249 100644
--- a/osdep/EthernetTap.cpp
+++ b/osdep/EthernetTap.cpp
@@ -41,6 +41,10 @@
#include "BSDEthernetTap.hpp"
#endif // __FreeBSD__
+#ifdef __sun
+#include "BSDEthernetTap.hpp"
+#endif // __sun
+
#ifdef __NetBSD__
#include "NetBSDEthernetTap.hpp"
#endif // __NetBSD__
@@ -127,6 +131,10 @@ std::shared_ptr<EthernetTap> EthernetTap::newInstance(
return std::shared_ptr<EthernetTap>(new BSDEthernetTap(homePath, concurrency, pinning, mac, mtu, metric, nwid, friendlyName, handler, arg));
#endif // __FreeBSD__
+#ifdef __sun
+ return std::shared_ptr<EthernetTap>(new BSDEthernetTap(homePath, concurrency, pinning, mac, mtu, metric, nwid, friendlyName, handler, arg));
+#endif // __sun
+
#ifdef __NetBSD__
return std::shared_ptr<EthernetTap>(new NetBSDEthernetTap(homePath, mac, mtu, metric, nwid, friendlyName, handler, arg));
#endif // __NetBSD__
diff --git a/osdep/ManagedRoute.cpp b/osdep/ManagedRoute.cpp
index 6a605b8..5386ffe 100644
--- a/osdep/ManagedRoute.cpp
+++ b/osdep/ManagedRoute.cpp
@@ -96,6 +96,50 @@ struct _RTE {
bool isDefault;
};
+#ifdef __sun // -------------------------------------------------------------
+#define ZT_ROUTING_SUPPORT_FOUND 1
+
+static bool _isHostRoute(const InetAddress& target)
+{
+ return ((target.ss_family == AF_INET) && (target.netmaskBits() >= 32)) || ((target.ss_family == AF_INET6) && (target.netmaskBits() >= 128));
+}
+
+static void _routeCmdSun(const char* op, const InetAddress& target, const InetAddress& via, const InetAddress& src, const char* localInterface)
+{
+ long p = (long)fork();
+ if (p > 0) {
+ int exitcode = -1;
+ ::waitpid(p, &exitcode, 0);
+ }
+ else if (p == 0) {
+ ::close(STDOUT_FILENO);
+ ::close(STDERR_FILENO);
+ char targetbuf[64];
+ char gwbuf[64];
+ const bool hostRoute = _isHostRoute(target);
+
+ if (via) {
+ if (hostRoute) {
+ ::execl(ZT_BSD_ROUTE_CMD, ZT_BSD_ROUTE_CMD, "-n", op, "-host", target.toIpString(targetbuf), via.toIpString(gwbuf), (const char*)0);
+ }
+ else {
+ ::execl(ZT_BSD_ROUTE_CMD, ZT_BSD_ROUTE_CMD, "-n", op, "-net", target.toString(targetbuf), via.toIpString(gwbuf), (const char*)0);
+ }
+ }
+ else if ((localInterface) && (localInterface[0]) && src) {
+ if (hostRoute) {
+ ::execl(ZT_BSD_ROUTE_CMD, ZT_BSD_ROUTE_CMD, "-n", op, "-host", target.toIpString(targetbuf), src.toIpString(gwbuf), "-interface", "-ifp", localInterface, (const char*)0);
+ }
+ else {
+ ::execl(ZT_BSD_ROUTE_CMD, ZT_BSD_ROUTE_CMD, "-n", op, "-net", target.toString(targetbuf), src.toIpString(gwbuf), "-interface", "-ifp", localInterface, (const char*)0);
+ }
+ }
+ ::_exit(-1);
+ }
+}
+
+#endif // __sun -------------------------------------------------------------
+
#ifdef __BSD__ // ------------------------------------------------------------
#define ZT_ROUTING_SUPPORT_FOUND 1
@@ -564,6 +608,19 @@ bool ManagedRoute::sync()
#endif // __BSD__ ------------------------------------------------------------
+#ifdef __sun // -------------------------------------------------------------
+
+ if ((leftt) && (!_applied.count(leftt))) {
+ _applied[leftt] = true;
+ _routeCmdSun("add", leftt, _via, _src, _device);
+ }
+ if ((rightt) && (!_applied.count(rightt))) {
+ _applied[rightt] = true;
+ _routeCmdSun("add", rightt, _via, _src, _device);
+ }
+
+#endif // __sun -------------------------------------------------------------
+
#ifdef __LINUX__ // ----------------------------------------------------------
#ifdef ZT_EXTOSDEP
@@ -612,6 +669,9 @@ void ManagedRoute::remove()
#ifdef __BSD__
#endif // __BSD__ ------------------------------------------------------------
+#ifdef __sun
+#endif // __sun -------------------------------------------------------------
+
for (std::map<InetAddress, bool>::iterator r(_applied.begin()); r != _applied.end(); ++r) {
#ifdef __BSD__ // ------------------------------------------------------------
if (_target && _target.netmaskBits() == 0) {
@@ -628,6 +688,10 @@ void ManagedRoute::remove()
break;
#endif // __BSD__ ------------------------------------------------------------
+#ifdef __sun // -------------------------------------------------------------
+ _routeCmdSun("delete", r->first, _via, _src, _device);
+#endif // __sun -------------------------------------------------------------
+
#ifdef __LINUX__ // ----------------------------------------------------------
//_routeCmd("del",r->first,_via,(_via) ? (const char *)0 : _device);
#ifdef ZT_EXTOSDEP
diff --git a/osdep/OSUtils.cpp b/osdep/OSUtils.cpp
index 2fee8f1..d1cd3a7 100644
--- a/osdep/OSUtils.cpp
+++ b/osdep/OSUtils.cpp
@@ -117,6 +117,8 @@ std::vector<std::string> OSUtils::listDirectory(const char* path, bool includeDi
#else
struct dirent de;
struct dirent* dptr;
+ struct stat st;
+ char tmp[4096];
DIR* d = opendir(path);
if (! d)
return r;
@@ -125,8 +127,18 @@ std::vector<std::string> OSUtils::listDirectory(const char* path, bool includeDi
if (readdir_r(d, &de, &dptr))
break;
if (dptr) {
- if ((strcmp(dptr->d_name, ".")) && (strcmp(dptr->d_name, "..")) && ((dptr->d_type != DT_DIR) || (includeDirectories)))
- r.push_back(std::string(dptr->d_name));
+ if ((strcmp(dptr->d_name, ".")) && (strcmp(dptr->d_name, ".."))) {
+ bool isDir = false;
+#if defined(__sun) || defined(__sun__)
+ ztsnprintf(tmp, sizeof(tmp), "%s/%s", path, dptr->d_name);
+ if ((stat(tmp, &st) == 0) && S_ISDIR(st.st_mode))
+ isDir = true;
+#else
+ isDir = (dptr->d_type == DT_DIR);
+#endif
+ if ((! isDir) || includeDirectories)
+ r.push_back(std::string(dptr->d_name));
+ }
}
else
break;
@@ -177,13 +189,24 @@ long OSUtils::cleanDirectory(const char* path, const int64_t olderThan)
if (readdir_r(d, &de, &dptr))
break;
if (dptr) {
- if ((strcmp(dptr->d_name, ".")) && (strcmp(dptr->d_name, "..")) && (dptr->d_type == DT_REG)) {
+ if ((strcmp(dptr->d_name, ".")) && (strcmp(dptr->d_name, ".."))) {
+ bool isRegular = false;
+#if defined(__sun) || defined(__sun__)
ztsnprintf(tmp, sizeof(tmp), "%s/%s", path, dptr->d_name);
- if (stat(tmp, &st) == 0) {
- int64_t mt = (int64_t)(st.st_mtime);
- if ((mt > 0) && ((mt * 1000) < olderThan)) {
- if (unlink(tmp) == 0)
- ++cleaned;
+ if ((stat(tmp, &st) == 0) && S_ISREG(st.st_mode))
+ isRegular = true;
+#else
+ isRegular = (dptr->d_type == DT_REG);
+ if (isRegular)
+ ztsnprintf(tmp, sizeof(tmp), "%s/%s", path, dptr->d_name);
+#endif
+ if (isRegular) {
+ if (stat(tmp, &st) == 0) {
+ int64_t mt = (int64_t)(st.st_mtime);
+ if ((mt > 0) && ((mt * 1000) < olderThan)) {
+ if (unlink(tmp) == 0)
+ ++cleaned;
+ }
}
}
}
diff --git a/service/OneService.cpp b/service/OneService.cpp
index 227b575..9f2c47c 100644
--- a/service/OneService.cpp
+++ b/service/OneService.cpp
@@ -3147,7 +3147,7 @@ class OneServiceImpl : public OneService {
// Ignore routes implied by local managed IPs since adding the IP adds the route.
// Apple on the other hand seems to need this at least on some versions.
-#ifndef __APPLE__
+#if !defined(__APPLE__) && !defined(__sun)
bool haveRoute = false;
for (std::vector<InetAddress>::iterator ip(n.managedIps().begin()); ip != n.managedIps().end(); ++ip) {
if ((target->netmaskBits() == ip->netmaskBits()) && (target->containsAddress(*ip))) {첫 번째는 빌드 시스템 보정이다. make-bsd.mk 에서 uname -s 로 SunOS 를 감지하고, SunOS일 때 -D__UNIX_LIKE__ 를 추가하며, 링크 단계에 -lsocket -lnsl -ldlpi 를 넣는다. 이건 Solaris/illumos 계열의 전통적인 네트워크/이름해석/데이터링크 라이브러리 구성을 ZeroTier가 알도록 만드는 작업이다. 같은 패치에서 SunOS 경로는 -fPIE, -pie -Wl,-z,relro,-z,now, strip --strip-all 조합을 피하고 일반 strip 만 쓰도록 바뀐다.
두 번째는 플랫폼 판별과 작은 CLI 안정화 보정이다. node/Constants.hpp 에서는 __sun 에서도 __UNIX_LIKE__ 가 잡히도록 하고, one.cpp 에서는 bond 상태 출력에 쓰는 bondingPolicyCode, numAliveLinks, numTotalLinks 를 int8_t 직접 변환이 아니라 OSUtils::jsonInt() 로 읽도록 바꾼다. 이 변경은 사소해 보이지만, JSON 숫자 타입을 8비트 정수에 직접 좁히는 과정에서 생길 수 있는 플랫폼별 타입 변환 문제를 피한다.
세 번째, 그리고 가장 중요한 부분은 osdep/BSDEthernetTap.cpp 의 SunOS 전용 데이터 경로다. 이 patch는 BSD Ethernet tap 구현 안에 SunOS 분기를 추가하고, libdlpi·stropts.h·sys/dlpi.h·sys/sockio.h 를 끌어들인다. 그리고 ZeroTier One 네트워크 ID에서 파생한 이름으로 세 종류의 링크를 만든다.
ztXXXXXXXXXXXXX: ZeroTier가 외부에 보여 줄 가시 VNICzbXXXXXXXXXXXXX: 프레임 송신용 백엔드 VNICzsXXXXXXXXXXXXX: 두 VNIC를 매단 etherstub
이 naming scheme은 뒤에서 볼 SMF 래퍼 스크립트와 정확히 맞물린다. 래퍼는 zt..., zb..., zs... 패턴을 보고 stale link를 정리하는데, 그 전제가 바로 이 patch 안에 박혀 있다.
patch의 _sunCreateDlpiStack() 는 실제로 다음 순서로 움직인다. 먼저 과거에 남았을 수 있는 ifconfig unplumb, dladm delete-vnic, dladm delete-etherstub 를 모두 시도해 stale 상태를 지운다. 그 다음 create-etherstub 으로 가상 스위치를 만들고, 그 위에 visible VNIC와 backend VNIC 를 각각 다른 MAC 으로 생성한다. visible 쪽은 ifconfig ... plumb mtu ... up 으로 올리고, 수신은 dlpi_open(..., DLPI_RAW) 와 promiscuous 모드로 여는 RX handle 에서 처리하며, 송신은 별도의 TX handle 에서 dlpi_send() 로 밀어 넣는다. 즉, Linux/FreeBSD 의 TAP 디바이스 파일을 읽고 쓰는 모델을 OmniOS 의 DLPI raw handle 모델로 치환한 것이다. OmniOS 의 dladm 문서가 설명하는 etherstub/VNIC 구조를 그대로 응용한 설계라고 보면 된다.
네 번째는 IP 주소와 라우팅 처리 보정이다. SunOS 분기에서는 BSD식 alias/-alias 가 아니라 ifconfig addif, removeif, inet6 plumb 같은 Solaris 계열 문법을 사용한다. ips() 구현은 ifaddrs 이름이 ztxxxx:1 같은 형태로 노출될 수 있는 점을 고려해 exact match 대신 prefix match 를 사용한다. ManagedRoute.cpp 에는 아예 SunOS 전용 _routeCmdSun() 이 들어가서 route -n add/delete -host/-net ... -ifp <iface> 형태의 명령을 실행하도록 한다. 그리고 service/OneService.cpp 에서는 Apple 이 아닌 플랫폼에서 “managed IP 가 있으면 route 는 이미 있다고 간주”하던 기존 최적화에서 __sun 을 제외한다.
마지막으로 OSUtils.cpp 에서는 dirent.d_type 에 의존하던 디렉터리/정규파일 판별을 SunOS 에서는 stat() 기반으로 바꾼다.
정리하면 이 patch의 본질은 이렇다. “ZeroTier One 을 SunOS 에서 억지로 Linux 처럼 돌리게 만드는 것”이 아니라, OmniOS 의 링크 계층 모델을 ZeroTier의 BSD 쪽 abstraction 에 접목시키는 것이다.
4. 빌드
PATH="/usr/bin:/usr/sbin:/opt/gcc-14/bin:/usr/gnu/bin:$PATH"
gmake OSTYPE=OpenBSD CC=gcc CXX=g++ -j1 one첫 줄은 현재 셸에서 사용할 PATH 를 재정의하는 단계다. gcc14 와 GNU 유틸리티가 기본보다 앞에 오도록 해서, 다음 줄의 gmake 가 의도한 툴체인 조합으로 실행되게 만든다.
둘째 줄은 이 구축의 핵심 빌드 명령이다. OSTYPE=OpenBSD 를 주는 이유는 ZeroTier upstream 이 OmniOS 를 위한 별도 make profile 을 제공하지 않기 때문이다. 대신 BSD 계열 경로를 재사용하고, 앞에서 적용한 patch 로 SunOS 전용 분기를 보강한다. ZeroTier One 의 공식 build.md 도 FreeBSD/OpenBSD에서는 gmake 를 사용한다고 설명하고, source build 후에는 zerotier-one -d 로 서비스를 띄우며, 기본 로컬 서비스 API는 127.0.0.1:9993 을 사용한다고 적고 있다. 또한 authtoken.secret 이 home folder에 저장된다는 점도 명시한다. 이 글의 SMF 래퍼가 바로 그 동작 모델 위에 올라간다.
5. 바이너리 설치와 런타임 디렉터리 준비
mkdir -p /opt/zerotier-one/bin
mkdir -p /opt/ooce/bin
mkdir -p /var/lib/zerotier-one
cp /opt/ZeroTierOne/zerotier-one /opt/zerotier-one/bin/zerotier-one
chmod 755 /opt/zerotier-one/bin/zerotier-one
ln -sf /opt/zerotier-one/bin/zerotier-one /opt/zerotier-one/bin/zerotier-cli
ln -sf /opt/zerotier-one/bin/zerotier-one /opt/zerotier-one/bin/zerotier-idtool
ln -sf /opt/zerotier-one/bin/zerotier-one /opt/ooce/bin/zerotier-one
ln -sf /opt/zerotier-one/bin/zerotier-one /opt/ooce/bin/zerotier-cli
ln -sf /opt/zerotier-one/bin/zerotier-one /opt/ooce/bin/zerotier-idtool이 단계는 소스 트리와 실제 실행 경로를 분리하는 단계다. /opt/ZeroTierOne 은 빌드 작업용 소스 트리이고, /opt/zerotier-one/bin 은 실제 서비스가 참조하는 정식 실행 경로다. 따라서 이후 patch를 다시 적용하거나 소스를 지워도, 설치된 서비스 바이너리 경로는 흔들리지 않는다.
zerotier-cli 와 zerotier-idtool 을 별도 바이너리처럼 설치하지 않고 심볼릭 링크로 처리하는 이유는, 공식 build 문서가 설명하듯 source build 후 서비스는 zerotier-one -d 로 시작되고, 제어는 로컬 JSON API를 통해 이뤄진다. 즉, 실질적인 핵심 실행 파일은 zerotier-one 하나고, 나머지는 그 API를 쓰는 다른 호출 이름일 뿐이다.
여기서 /var/lib/zerotier-one 을 미리 만들어 두는 점도 중요하다. 공식 build 문서는 source build 시 home folder 에 상태와 설정이 저장되고, authtoken.secret 도 그 안에 생성된다고 설명한다. Linux 기본 경로로는 /var/lib/zerotier-one, FreeBSD/OpenBSD 기본 경로로는 /var/db/zerotier-one 을 들고 있는데, 이 OmniOS 구축은 의도적으로 Linux형 경로를 채택했다. 뒤에서 내려받는 SMF 스크립트와 manifest 도 모두 그 위치를 기준으로 고정돼 있으므로, 경로 일관성이 매우 중요하다.
또 /opt/ooce/bin 에 같은 링크를 하나 더 두는 이유는 운영 편의성이다. 곧 내려받을 SMF 래퍼 스크립트가 PATH=/usr/bin:/usr/sbin:/opt/ooce/bin 을 사용하기 때문에, 서비스 제어 스크립트가 zerotier-cli 를 찾을 때도 별도 절대경로 계산 없이 바로 동작한다.
6. SMF method script 설치
wget https://www.ourdare.com/resources/zerotier-one-smf -O /opt/zerotier-one/bin/zerotier-one-smf
chmod 755 /opt/zerotier-one/bin/zerotier-one-smf#!/usr/bin/bash
set -euo pipefail
PATH=/usr/bin:/usr/sbin:/opt/ooce/bin
ZT_BIN="/opt/zerotier-one/bin/zerotier-one"
ZT_HOME="/var/lib/zerotier-one"
ZT_MATCH="${ZT_BIN}.*${ZT_HOME}"
cleanup_owned_links() {
typeset link
while IFS=: read -r link; do
case "$link" in
zt[0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v])
ifconfig "$link" inet6 unplumb >/dev/null 2>&1 || true
ifconfig "$link" unplumb >/dev/null 2>&1 || true
dladm delete-vnic "$link" >/dev/null 2>&1 || true
;;
zb[0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v])
dladm delete-vnic "$link" >/dev/null 2>&1 || true
;;
esac
done < <(dladm show-vnic -p -o LINK 2>/dev/null || true)
while IFS=: read -r link; do
case "$link" in
zs[0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v][0-9a-v])
dladm delete-etherstub "$link" >/dev/null 2>&1 || true
;;
esac
done < <(dladm show-etherstub -p -o LINK 2>/dev/null || true)
}
rejoin_saved_networks() {
typeset network_file
typeset nwid
if [ ! -d "${ZT_HOME}/networks.d" ]; then
return 0
fi
for network_file in "${ZT_HOME}"/networks.d/*.conf; do
if [ ! -f "${network_file}" ]; then
continue
fi
nwid="${network_file##*/}"
nwid="${nwid%.conf}"
if [ -n "${nwid}" ]; then
zerotier-cli join "${nwid}" >/dev/null 2>&1 || true
fi
done
}
start_service() {
typeset attempt=0
if pgrep -f "${ZT_MATCH}" >/dev/null 2>&1; then
exit 0
fi
cleanup_owned_links
"${ZT_BIN}" -d "${ZT_HOME}"
while [ "${attempt}" -lt 20 ]; do
if zerotier-cli info >/dev/null 2>&1; then
rejoin_saved_networks
exit 0
fi
attempt=$((attempt + 1))
sleep 1
done
exit 1
}
stop_service() {
typeset attempt=0
if ! pgrep -f "${ZT_MATCH}" >/dev/null 2>&1; then
cleanup_owned_links
exit 0
fi
pkill -TERM -f "${ZT_MATCH}" || true
while [ "${attempt}" -lt 20 ]; do
if ! pgrep -f "${ZT_MATCH}" >/dev/null 2>&1; then
cleanup_owned_links
exit 0
fi
attempt=$((attempt + 1))
sleep 1
done
pkill -KILL -f "${ZT_MATCH}" || true
cleanup_owned_links
}
case "${1:-}" in
start)
start_service
;;
stop)
stop_service
;;
restart)
stop_service
start_service
;;
*)
printf 'Usage: %s [start|stop|restart]\n' "$0" >&2
exit 2
;;
esac먼저, ZT_BIN="/opt/zerotier-one/bin/zerotier-one", ZT_HOME="/var/lib/zerotier-one"로 선언해 두었고, 프로세스 식별용으로 ZT_MATCH="${ZT_BIN}.*${ZT_HOME}" 패턴을 만든다. “어느 zerotier-one 이든”이 아니라 특정 바이너리와 특정 홈 디렉터리 조합으로 실행된 프로세스만 자기 것으로 간주하기 위함이다.
cleanup_owned_links() 는 dladm show-vnic -p -o LINK 와 dladm show-etherstub -p -o LINK 출력 중에서, zt, zb, zs 로 시작하고 뒤에 13자리 base32 네트워크 식별자가 붙는 이름만 골라 삭제하며, zt... 링크는 먼저 ifconfig inet6 unplumb , ifconfig unplumb 를 시도한 뒤 dladm delete-vnic 를 호출한다. 이 동작은 앞서 설명한 patch 의 naming scheme 과 정확히 맞물린다. 즉, patch 가 네트워크별 zt/zb/zs 링크 세트를 생성하고, SMF 스크립트는 그 세트만 골라 정리한다.
rejoin_saved_networks() 는 ${ZT_HOME}/networks.d/*.conf 를 훑어서 파일명에서 network ID를 뽑은 다음, zerotier-cli join <nwid> 를 다시 수행한다. ZeroTier One 공식 구성 문서는 설치 후 네트워크를 16자리 network ID로 join하고, 각 네트워크가 시스템에 가상 인터페이스로 나타난다고 설명한다. 이 SMF 래퍼는 그 동작 모델을 서비스 재기동에도 이어붙이기 위해 networks.d를 재해석하는 것이다.
start_service() 는 이미 동일 프로세스가 떠 있으면 성공으로 종료한다. 그렇지 않으면 먼저 stale 링크를 정리하고, "${ZT_BIN}" -d "${ZT_HOME}" 로 daemon을 올린 뒤, 최대 20초 동안 zerotier-cli info 가 살아날 때까지 폴링한다. zerotier-cli info 가 성공한다는 것은 daemon 프로세스 자체가 떴고, 로컬 제어 API도 살아 있으며, 토큰 접근 경로도 일치한다는 의미다.
stop_service() 는 TERM → 대기 → KILL → cleanup 순서로 진행한다. 정상 종료를 먼저 시도하고, 실패하면 강제 종료하되 마지막에는 항상 stale 링크를 걷어 낸다.
7. SMF manifest 설치
mkdir -p /var/svc/manifest/network
wget https://www.ourdare.com/resources/zerotier-one.xml -O /var/svc/manifest/network/zerotier-one.xml<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<service_bundle type="manifest" name="zerotier-one">
<service name="network/zerotier-one" type="service" version="1">
<create_default_instance enabled="false"/>
<single_instance/>
<dependency name="fs-local" grouping="require_all" restart_on="none" type="service">
<service_fmri value="svc:/system/filesystem/local:default"/>
</dependency>
<dependency name="network-physical" grouping="optional_all" restart_on="none" type="service">
<service_fmri value="svc:/network/physical:default"/>
</dependency>
<exec_method type="method" name="start" exec="/opt/zerotier-one/bin/zerotier-one-smf start" timeout_seconds="120"/>
<exec_method type="method" name="stop" exec="/opt/zerotier-one/bin/zerotier-one-smf stop" timeout_seconds="120"/>
<property_group name="startd" type="framework">
<propval name="duration" type="astring" value="transient"/>
</property_group>
<stability value="Unstable"/>
<template>
<common_name>
<loctext xml:lang="C">ZeroTier One</loctext>
</common_name>
<description>
<loctext xml:lang="C">ZeroTier One service for OmniOS r151054</loctext>
</description>
</template>
</service>
</service_bundle>이 manifest 는 SMF에 ZeroTier One 을 정식 서비스 인스턴스로 등록하기 위한, service bundle이 SMF 서비스 설명을 담는 XML 문서이다.
service name="network/zerotier-one" 로 서비스 이름을 고정했고, create_default_instance enabled="false" 와 single_instance 를 선언했다. import 직후 자동 기동되지는 않으며, 인스턴스는 하나만 가진다. 또한 fs-local 은 require_all 의존성으로 걸려 있어 로컬 파일시스템이 준비돼야 하고, network-physical 은 optional_all 로 걸려 있어 물리 네트워크 쪽과 느슨하게 연동된다. start/stop은 /opt/zerotier-one/bin/zerotier-one-smf 를 호출하며, startd 프레임워크 그룹에는 duration=transient 가 들어 있다. 이는 이 서비스가 wrapper가 데몬을 띄운 뒤 종료하는 방식으로 설계됐음을 보여 준다. 파일 설명에도 서비스 이름은 “ZeroTier One” , 설명은 “ZeroTier One service for OmniOS r151054” 로 명시했다.
8. 서비스 등록
svccfg import /var/svc/manifest/network/zerotier-one.xml
svcadm enable -r svc:/network/zerotier-one:defaultsvccfg import 는 manifest 를 SMF repository 에 반영하며, svcadm enable -r 는 서비스를 등록하고 실행한다.
9. 검증
svcs -xv zerotier-one
zerotier-cli infosvcs -xv zerotier-one 는 OmniOS/SMF 기준의 서비스 상태 확인이고, zerotier-cli info 는 ZeroTier One 기준의 실제 health check 다. 최신 CLI 문서는 ONLINE, OFFLINE, TUNNELED 를 다음과 같이 설명한다.
ONLINE: global root infrastructure와 통신 가능OFFLINE: root infrastructure에 닿지 못하지만 재시도 중TUNNELED: UDP/9993로 직접 통신하지 못해 TCP fallback relay 사용 중
즉, 설치 직후 zerotier-cli info 에서 ONLINE 이 보이면, 바이너리 실행, 홈 디렉터리, 토큰, 로컬 API, 기본 외부 통신까지 전부 통과했다는 뜻이다.
선택 사항. 라우터나 중계 노드로 사용하기
routeadm -e ipv4-forwarding -u
routeadm -e ipv6-forwarding -u
routeadm | egrep 'IPv4 forwarding|IPv6 forwarding'이 노드를 단순 endpoint 가 아니라 ZeroTier managed route 를 중계하는 Router 로 쓸 계획이라면, ZeroTier One 문서가 설명하듯 “Managed Route 설정 + OS forwarding 활성화”가 함께 필요하다. OmniOS 쪽에서는 routeadm 으로 처리하는 게 가장 직관적이다. routeadm 은 시스템 전역 IP forwarding 과 routing 을 관리하며, -u는 현재 구성 값을 즉시 running system에 적용한다.
참고 사이트
위 튜토리얼은 2026년 3월 14일 기준 전체 패키지 업데이트가 적용된 OmniOS r151054r LTS Release 와 ZeroTierOne clone 코드 기준으로 작성되었으며, 차후 OmniOS 혹은 ZeroTierOne 버전 차이에 따라 일부 수정이 필요할 수 있다.