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.patch
omnios-zerotier-one.patch
diff --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 -sSunOS 를 감지하고, 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, numTotalLinksint8_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가 외부에 보여 줄 가시 VNIC
  • zbXXXXXXXXXXXXX : 프레임 송신용 백엔드 VNIC
  • zsXXXXXXXXXXXXX : 두 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-clizerotier-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
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 LINKdladm 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
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-localrequire_all 의존성으로 걸려 있어 로컬 파일시스템이 준비돼야 하고, network-physicaloptional_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:default

svccfg import 는 manifest 를 SMF repository 에 반영하며, svcadm enable -r 는 서비스를 등록하고 실행한다.

9. 검증

svcs -xv zerotier-one
zerotier-cli info

svcs -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 버전 차이에 따라 일부 수정이 필요할 수 있다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Leave the field below empty!