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://github.com/itinfra7/zerotier-one-omnios/releases/latest/download/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 cf4c22c8b..67a811ea2 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 6baf8c7fe..a76d2345c 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
@@ -287,7 +293,11 @@
 /**
  * Default MTU used for Ethernet tap device
  */
+#if defined(__sun) || defined(__sun__)
+#define ZT_DEFAULT_MTU 1280
+#else
 #define ZT_DEFAULT_MTU 2800
+#endif
 
 /**
  * Maximum number of packet fragments we'll support (protocol max: 16)
diff --git a/one.cpp b/one.cpp
index a066f4e65..8cffbbf53 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 82ac07d1a..727d4d9aa 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,184 @@
 #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 unsigned int _sunClampMtu(unsigned int mtu)
+{
+	/*
+	 * The illumos DLPI/VNIC path used here is stable with conservative MTUs,
+	 * but larger values can stall encrypted TCP sessions over routed links.
+	 */
+	if (mtu < 1280U)
+		return 1280U;
+	if (mtu > 1280U)
+		return 1280U;
+	return mtu;
+}
+
+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)
+{
+	const unsigned int effectiveMtu = _sunClampMtu(mtu);
+	std::string mtuStr(std::to_string(effectiveMtu));
+	std::string mtuProp(std::string("mtu=") + mtuStr);
+	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/dladm", "/usr/sbin/dladm", "set-linkprop", "-p", mtuProp.c_str(), visDev.c_str()}) != 0)
+		return false;
+	if (_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "set-linkprop", "-p", mtuProp.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,
@@ -70,9 +245,26 @@ BSDEthernetTap::BSDEthernetTap(
 	, _pinning(pinning)
 	, _arg(arg)
 	, _nwid(nwid)
-	, _mtu(mtu)
+	, _mtu(
+#ifdef __sun
+		  _sunClampMtu(mtu)
+#else
+		  mtu
+#endif
+	  )
 	, _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 +273,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 +363,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 +375,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 +392,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 +405,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 +424,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 +462,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 +507,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 +579,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 +624,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 +647,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 +662,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)
@@ -410,6 +685,9 @@ void BSDEthernetTap::scanMulticastGroups(std::vector<MulticastGroup>& added, std
 
 void BSDEthernetTap::setMtu(unsigned int mtu)
 {
+#ifdef __sun
+	mtu = _sunClampMtu(mtu);
+#endif
 	if (mtu != _mtu) {
 		_mtu = mtu;
 		long cpid = (long)vfork();
@@ -418,6 +696,11 @@ void BSDEthernetTap::setMtu(unsigned int mtu)
 			OSUtils::ztsnprintf(tmp, sizeof(tmp), "%u", mtu);
 #ifdef ZT_TRACE
 			fprintf(stderr, "DEBUG: ifconfig %s mtu %s" ZT_EOL_S, _dev.c_str(), tmp);
+#endif
+#ifdef __sun
+			std::string mtuProp(std::string("mtu=") + tmp);
+			(void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "set-linkprop", "-p", mtuProp.c_str(), _dev.c_str()});
+			(void)_sunRun({"/usr/sbin/dladm", "/usr/sbin/dladm", "set-linkprop", "-p", mtuProp.c_str(), _sunBackendDev.c_str()});
 #endif
 			execl("/sbin/ifconfig", "/sbin/ifconfig", _dev.c_str(), "mtu", tmp, (const char*)0);
 			_exit(-1);
@@ -435,11 +718,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 +737,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 +761,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 +798,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 190f3a0b4..d5ed8ee68 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 1d35e01dc..cf5224963 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 6a605b856..5386ffef4 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 2fee8f194..d1cd3a7d2 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 227b575f7..9f2c47c29 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))) {
소스 보기

이 단계에서는 OmniOS에서 필요한 소스 수정 파일을 적용한다. 파일명은 omnios-zerotier-one.patch이지만, 문서에서는 편의상 OmniOS용 수정 파일이라고 부르겠다. 이 파일은 ZeroTier One을 OmniOS의 링크 계층과 서비스 모델에 맞춰 빌드하고 동작시키기 위해 필요한 내용들을 모아 둔 것이다.

첫 번째는 빌드 시스템 정리다. make-bsd.mk에서 uname -s로 SunOS를 감지하고, SunOS일 때 -D__UNIX_LIKE__를 추가하며, 링크 단계에 -lsocket, -lnsl, -ldlpi를 넣는다. Solaris와 illumos 계열에서 필요한 네트워크, 이름해석, 데이터링크 라이브러리 구성을 정확히 알려 주기 위한 변경이다. 같은 파일에서 SunOS 경로는 기본 BSD와 Linux 빌드가 쓰는 -fPIE, -pie -Wl,-z,relro,-z,now, strip --strip-all 조합을 그대로 쓰지 않고, 일반 strip 중심으로 정리되어 있다.

두 번째는 플랫폼 판별과 CLI 출력 보정이다. node/Constants.hpp에서는 __sun에서도 __UNIX_LIKE__가 잡히도록 하고, SunOS 기본 MTU를 2800이 아니라 1280으로 둔다. one.cpp에서는 bond 상태 출력에 쓰는 bondingPolicyCode, numAliveLinks, numTotalLinksint8_t 직접 변환이 아니라 OSUtils::jsonInt()로 읽도록 바꿨다. 이런 부분은 눈에 확 띄는 기능 추가는 아니지만, 플랫폼별 타입 처리 차이를 줄이는 데 도움이 된다.

세 번째, 그리고 가장 큰 부분은 osdep/BSDEthernetTap.cpposdep/BSDEthernetTap.hpp의 SunOS용 데이터 경로다. 여기서는 BSD Ethernet tap 구현 안에 SunOS 분기를 추가하고, libdlpi, stropts.h, sys/dlpi.h, sys/sockio.h를 끌어들인다. 그리고 ZeroTier 네트워크 ID에서 파생한 이름으로 세 종류의 링크를 만든다.

  • ztXXXXXXXXXXXXX : ZeroTier가 외부에 보여 줄 visible VNIC
  • zbXXXXXXXXXXXXX : 프레임 송신용 backend VNIC
  • zsXXXXXXXXXXXXX : 두 VNIC를 매다는 etherstub

이 naming scheme은 뒤에서 설치할 SMF method script와 정확히 맞물린다. method script는 zt..., zb..., zs... 패턴을 보고 관련 링크만 정리하고, 소스 쪽은 같은 이름 규칙으로 링크를 만든다.

수정 파일의 흐름을 따라가 보면 구성 순서도 분명하다. 먼저 과거 실행에서 남았을 수 있는 ifconfig unplumb, dladm delete-vnic, dladm delete-etherstub를 시도해 이전 링크 상태를 정리한다. 그 다음 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 모델로 바꾼 형태다.

네 번째는 MTU 처리다. SunOS 경로에서는 ZeroTier 내부 MTU와 visible VNIC, backend VNIC의 MTU를 모두 1280 기준으로 맞추도록 정리되어 있다. ZT_DEFAULT_MTU도 SunOS에서는 1280으로 바뀌고, 인터페이스를 다시 구성할 때도 같은 값이 유지된다. 네트워크 장치 이름과 링크 구성이 맞더라도 내부 MTU 상태와 실제 인터페이스 MTU가 어긋나면 예상하지 못한 동작이 나올 수 있기 때문에, 이 부분을 함께 맞춰 두는 구성이 안정적이다.

다섯 번째는 주소와 라우팅 처리다. SunOS 경로에서는 BSD식 alias, -alias 대신 ifconfig addif, removeif, inet6 plumb 같은 Solaris 계열 문법을 사용한다. ips() 구현은 ifaddrs 이름이 ztxxxx:1 같은 형태로 보일 수 있는 점을 고려해 exact match 대신 prefix match를 사용한다. ManagedRoute.cpp에는 SunOS용 route 처리 경로가 들어가 있어 route -n add/delete -host/-net ... -ifp <iface> 형태로 명령을 실행한다. OSUtils.cpp에서는 dirent.d_type 대신 stat() 기반으로 파일 타입을 판별한다.

정리하면, 이 수정 파일은 ZeroTier One을 OmniOS에서 빌드하고 서비스로 운영할 수 있도록, OmniOS의 etherstub + vnic + libdlpi 구조에 맞춰 필요한 부분을 정리한 내용이라고 보면 된다.

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 계열 경로를 재사용하고, 앞에서 적용한 SunOS용 수정 내용을 함께 태워서 빌드한다.

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은 실제 서비스가 참조하는 정식 실행 경로다. 따라서 이후 소스를 다시 받아도 설치된 바이너리 경로는 그대로 유지할 수 있다.

zerotier-clizerotier-idtool을 별도 바이너리처럼 두지 않고 심볼릭 링크로 처리하는 이유는, 실질적인 핵심 실행 파일이 zerotier-one 하나이기 때문이다. /var/lib/zerotier-one은 상태 파일과 토큰, 네트워크 구성 파일이 들어갈 홈 디렉터리다. 뒤에서 설치할 method script와 manifest도 이 위치를 기준으로 고정되어 있다.

/opt/ooce/bin에 같은 링크를 하나 더 두는 이유는 운영 편의를 위해서다. method script가 PATH=/usr/bin:/usr/sbin:/opt/ooce/bin을 사용하므로, 서비스 제어 중에도 zerotier-cli를 바로 찾을 수 있다.

6. SMF method script 설치

wget https://github.com/itinfra7/zerotier-one-omnios/releases/latest/download/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를 호출한다. 이 동작은 앞에서 정리한 링크 naming scheme과 그대로 연결된다.

rejoin_saved_networks()${ZT_HOME}/networks.d/*.conf를 훑어서 파일명에서 network ID를 뽑은 다음, zerotier-cli join <nwid>를 다시 수행한다. 서비스 재기동 뒤에도 저장된 네트워크 구성을 이어가기 위한 부분이다.

start_service()는 이미 동일 프로세스가 떠 있으면 성공으로 종료한다. 그렇지 않으면 먼저 관련 링크를 정리하고, "${ZT_BIN}" -d "${ZT_HOME}"로 daemon을 올린 뒤 최대 20초 동안 zerotier-cli info가 살아날 때까지 폴링한다. zerotier-cli info가 성공한다는 것은 daemon 프로세스 자체가 떴고, 로컬 제어 API도 살아 있으며, 홈 디렉터리와 토큰 접근 경로도 일치한다는 뜻이다.

stop_service()TERM -> 대기 -> KILL -> cleanup 순서로 진행한다. 정상 종료를 먼저 시도하고, 실패하면 강제 종료하되 마지막에는 항상 관련 링크를 정리한다.

7. SMF manifest 설치

mkdir -p /var/svc/manifest/network
wget https://github.com/itinfra7/zerotier-one-omnios/releases/latest/download/zerotier-one.xml -O /var/svc/manifest/network/zerotier-one.xml
/usr/bin/perl -pi -e 's/\r//g' /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는 network/zerotier-one 서비스를 SMF에 등록하기 위한 XML 문서다.

service name="network/zerotier-one"로 서비스 이름을 고정했고, create_default_instance enabled="false"single_instance를 선언했다. import 직후 자동 기동되지는 않으며, 인스턴스는 하나만 가진다. 또한 fs-localrequire_all 의존성으로 걸려 있어 로컬 파일시스템이 준비돼야 하고, network-physicaloptional_all로 걸려 있어 물리 네트워크 쪽과 느슨하게 연결된다. startstop/opt/zerotier-one/bin/zerotier-one-smf를 호출하며, duration=transient로 설정되어 있어 wrapper가 daemon을 띄운 뒤 종료하는 구조를 따른다.

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!