diff --git a/.github/workflows/validate-linux.sh b/.github/workflows/validate-linux.sh
index 61670d670..a661f6f6c 100755
--- a/.github/workflows/validate-linux.sh
+++ b/.github/workflows/validate-linux.sh
@@ -20,6 +20,9 @@ mkdir $TEST_DIR_PREFIX
 # How long we will wait for ZT to come online before considering it a failure
 MAX_WAIT_SECS=30
 
+ZT_PORT_NODE_1=9996
+ZT_PORT_NODE_2=9997
+
 ################################################################################
 # Multi-node connectivity and performance test                                 #
 ################################################################################
@@ -99,14 +102,19 @@ test() {
 		--xml=yes \
 		--xml-file=$FILENAME_MEMORY_LOG \
 		--leak-check=full \
-		./zerotier-one node1 -p9996 -U >>node_1.log 2>&1 &
+		./zerotier-one node1 -p$ZT_PORT_NODE_1 -U >>node_1.log 2>&1 &
 
 	# Second instance, not run in memory profiler
 	# Don't set up internet access until _after_ zerotier is running
 	# This has been a source of stuckness in the past.
 	$NS2 ip addr del 192.168.1.2/24 dev veth3
-	$NS2 sudo ./zerotier-one node2 -U -p9997 >>node_2.log 2>&1 &
-	sleep 1;
+	$NS2 sudo ./zerotier-one node2 -U -p$ZT_PORT_NODE_2 >>node_2.log 2>&1 &
+
+	sleep 10; # New HTTP control plane is a bit sluggish, so we delay here
+
+	check_bind_to_correct_ports $ZT_PORT_NODE_1
+	check_bind_to_correct_ports $ZT_PORT_NODE_2
+
 	$NS2 ip addr add 192.168.1.2/24 dev veth3
 	$NS2 ip route add default via 192.168.1.1
 
@@ -458,4 +466,32 @@ check_exit_on_invalid_identity() {
 	fi
 }
 
+################################################################################
+# Check that we're binding to the primary port for TCP/TCP6/UDP                #
+################################################################################
+
+check_bind_to_correct_ports() {
+	PORT_NUMBER=$1
+	echo "Checking bound ports:"
+	sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier"
+	if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier" | grep "tcp") ]];
+	then
+		:
+	else
+		exit_test_and_generate_report $TEST_FAIL "ZeroTier did not bind to tcp/$1"
+	fi
+	if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier" | grep "tcp6") ]];
+	then
+		:
+	else
+		exit_test_and_generate_report $TEST_FAIL "ZeroTier did not bind to tcp6/$1"
+	fi
+	if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier" | grep "udp") ]];
+	then
+		:
+	else
+		exit_test_and_generate_report $TEST_FAIL "ZeroTier did not bind to udp/$1"
+	fi
+}
+
 test "$@"
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 1585aeead..86f3f1e8c 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -1,6 +1,12 @@
 ZeroTier Release Notes
 ======
 
+# 2023-08-25 -- Version 1.12.1
+
+  * Minor release to fix a port binding issue in Linux.
+  * Update Debian dependencies.
+  * No changes for other platforms.
+
 # 2023-08-23 -- Version 1.12.0
 
   * Experimental Windows ARM64 support
diff --git a/controller/EmbeddedNetworkController.cpp b/controller/EmbeddedNetworkController.cpp
index b60c375c4..ac1947ce4 100644
--- a/controller/EmbeddedNetworkController.cpp
+++ b/controller/EmbeddedNetworkController.cpp
@@ -863,9 +863,17 @@ std::string EmbeddedNetworkController::networkUpdateFromPostData(uint64_t networ
 
 void EmbeddedNetworkController::configureHTTPControlPlane(
 	httplib::Server &s,
+	httplib::Server &sv6,
 	const std::function<void(const httplib::Request&, httplib::Response&, std::string)> setContent)
 {
-	s.Get("/controller/network", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+	// Control plane Endpoints
+	std::string networkListPath = "/controller/network";
+	std::string networkPath = "/controller/network/([0-9a-fA-F]{16})";
+	std::string oldAndBustedNetworkCreatePath = "/controller/network/([0-9a-fA-F]{10})______";
+	std::string memberListPath = "/controller/network/([0-9a-fA-F]{16})/member";
+	std::string memberPath = "/controller/network/([0-9a-fA-F]{16})/member/([0-9a-fA-F]{10})";
+
+	auto networkListGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		std::set<uint64_t> networkIds;
 		_db.networks(networkIds);
 		char tmp[64];
@@ -877,9 +885,11 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 		}
 
 		setContent(req, res, out.dump());
-	});
+	};
+	s.Get(networkListPath, networkListGet);
+	sv6.Get(networkListPath, networkListGet);
 
-	s.Get("/controller/network/([0-9a-fA-F]{16})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+	auto networkGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		auto networkID = req.matches[1];
 		uint64_t nwid = Utils::hexStrToU64(networkID.str().c_str());
 		json network;
@@ -889,7 +899,9 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 		}
 
 		setContent(req, res, network.dump());
-	});
+	};
+	s.Get(networkPath, networkGet);
+	sv6.Get(networkPath, networkGet);
 
 	auto createNewNetwork = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		fprintf(stderr, "creating new network (new style)\n");
@@ -912,8 +924,10 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 
 		setContent(req, res, networkUpdateFromPostData(nwid, req.body));
 	};
-	s.Put("/controller/network", createNewNetwork);
-	s.Post("/controller/network", createNewNetwork);
+	s.Put(networkListPath, createNewNetwork);
+	s.Post(networkListPath, createNewNetwork);
+	sv6.Put(networkListPath, createNewNetwork);
+	sv6.Post(networkListPath, createNewNetwork);
 
 	auto createNewNetworkOldAndBusted = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		auto inID = req.matches[1].str();
@@ -941,10 +955,24 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 		}
 		setContent(req, res, networkUpdateFromPostData(nwid, req.body));
 	};
-	s.Put("/controller/network/([0-9a-fA-F]{10})______", createNewNetworkOldAndBusted);
-	s.Post("/controller/network/([0-9a-fA-F]{10})______", createNewNetworkOldAndBusted);
+	s.Put(oldAndBustedNetworkCreatePath, createNewNetworkOldAndBusted);
+	s.Post(oldAndBustedNetworkCreatePath, createNewNetworkOldAndBusted);
+	sv6.Put(oldAndBustedNetworkCreatePath, createNewNetworkOldAndBusted);
+	sv6.Post(oldAndBustedNetworkCreatePath, createNewNetworkOldAndBusted);
 
-	s.Delete("/controller/network/([0-9a-fA-F]{16})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+	auto networkPost = [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto networkID = req.matches[1].str();
+		uint64_t nwid = Utils::hexStrToU64(networkID.c_str());
+
+		res.status = 200;
+		setContent(req, res, networkUpdateFromPostData(nwid, req.body));
+	};
+	s.Put(networkPath, networkPost);
+	s.Post(networkPath, networkPost);
+	sv6.Put(networkPath, networkPost);
+	sv6.Post(networkPath, networkPost);
+
+	auto networkDelete = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		auto networkID = req.matches[1].str();
 		uint64_t nwid = Utils::hexStrToU64(networkID.c_str());
 
@@ -956,9 +984,11 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 
 		_db.eraseNetwork(nwid);
 		setContent(req, res, network.dump());
-	});
+	};
+	s.Delete(networkPath, networkDelete);
+	sv6.Delete(networkPath, networkDelete);
 
-	s.Get("/controller/network/([0-9a-fA-F]{16})/member", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+	auto memberListGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		auto networkID = req.matches[1];
 		uint64_t nwid = Utils::hexStrToU64(networkID.str().c_str());
 		json network;
@@ -982,9 +1012,11 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 		}
 
 		setContent(req, res, out.dump());
-	});
+	};
+	s.Get(memberListPath, memberListGet);
+	sv6.Get(memberListPath, memberListGet);
 
-	s.Get("/controller/network/([0-9a-fA-F]{16})/member/([0-9a-fA-F]{10})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+	auto memberGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		auto networkID = req.matches[1];
 		auto memberID = req.matches[2];
 		uint64_t nwid = Utils::hexStrToU64(networkID.str().c_str());
@@ -997,7 +1029,9 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 		}
 
 		setContent(req, res, member.dump());
-	});
+	};
+	s.Get(memberPath, memberGet);
+	sv6.Get(memberPath, memberGet);
 
 	auto memberPost = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		auto networkID = req.matches[1].str();
@@ -1102,10 +1136,12 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 
 		setContent(req, res, member.dump());
 	};
-	s.Put("/controller/network/([0-9a-fA-F]{16})/member/([0-9a-fA-F]{10})", memberPost);
-	s.Post("/controller/network/([0-9a-fA-F]{16})/member/([0-9a-fA-F]{10})", memberPost);
+	s.Put(memberPath, memberPost);
+	s.Post(memberPath, memberPost);
+	sv6.Put(memberPath, memberPost);
+	sv6.Post(memberPath, memberPost);
 
-	s.Delete("/controller/network/([0-9a-fA-F]{16})/member/([0-9a-fA-F]{10})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+	auto memberDelete = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 		auto networkID = req.matches[1].str();
 		auto memberID = req.matches[2].str();
 
@@ -1126,7 +1162,9 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
 		_db.eraseMember(nwid, address);
 
 		setContent(req, res, member.dump());
-	});
+	};
+	s.Delete(memberPath, memberDelete);
+	sv6.Delete(memberPath, memberDelete);
 }
 
 void EmbeddedNetworkController::handleRemoteTrace(const ZT_RemoteTrace &rt)
diff --git a/controller/EmbeddedNetworkController.hpp b/controller/EmbeddedNetworkController.hpp
index ef369be39..4ea00b65a 100644
--- a/controller/EmbeddedNetworkController.hpp
+++ b/controller/EmbeddedNetworkController.hpp
@@ -70,6 +70,7 @@ public:
 
 	void configureHTTPControlPlane(
 		httplib::Server &s,
+		httplib::Server &sV6,
 		const std::function<void(const httplib::Request&, httplib::Response&, std::string)>);
 
 	void handleRemoteTrace(const ZT_RemoteTrace &rt);
diff --git a/debian/changelog b/debian/changelog
index e03a94a20..0ba464399 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+zerotier-one (1.12.1) unstable; urgency=medium
+
+  * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko <adam.ierymenko@zerotier.com>  Fri, 25 Aug 2023 01:00:00 -0700
+
 zerotier-one (1.12.0) unstable; urgency=medium
 
   * See RELEASE-NOTES.md for release notes.
diff --git a/debian/control b/debian/control
index 195acefd7..b0c178ab1 100644
--- a/debian/control
+++ b/debian/control
@@ -10,7 +10,7 @@ Homepage: https://www.zerotier.com/
 
 Package: zerotier-one
 Architecture: any
-Depends:  iproute2, adduser, libstdc++6 (>= 5), openssl
+Depends: adduser, libstdc++6 (>= 5), openssl
 Homepage: https://www.zerotier.com/
 Description: ZeroTier network virtualization service
  ZeroTier One lets you join ZeroTier virtual networks and
diff --git a/debian/control.wheezy b/debian/control.wheezy
index a8e240847..fbbc40d93 100644
--- a/debian/control.wheezy
+++ b/debian/control.wheezy
@@ -10,7 +10,7 @@ Homepage: https://www.zerotier.com/
 
 Package: zerotier-one
 Architecture: any
-Depends:  ${shlibs:Depends}, ${misc:Depends}, iproute, libstdc++6
+Depends: ${shlibs:Depends}, ${misc:Depends}, libstdc++6
 Homepage: https://www.zerotier.com/
 Description: ZeroTier network virtualization service
  ZeroTier One lets you join ZeroTier virtual networks and
diff --git a/debian/rules b/debian/rules
old mode 100755
new mode 100644
diff --git a/debian/rules.wheezy b/debian/rules.wheezy
old mode 100755
new mode 100644
diff --git a/ext/installfiles/mac/ZeroTier One.pkgproj b/ext/installfiles/mac/ZeroTier One.pkgproj
index efae3e02b..67a4fd452 100755
--- a/ext/installfiles/mac/ZeroTier One.pkgproj	
+++ b/ext/installfiles/mac/ZeroTier One.pkgproj	
@@ -701,7 +701,7 @@
 				<key>USE_HFS+_COMPRESSION</key>
 				<false/>
 				<key>VERSION</key>
-				<string>1.12.0</string>
+				<string>1.12.1</string>
 			</dict>
 			<key>TYPE</key>
 			<integer>0</integer>
diff --git a/ext/installfiles/windows/ZeroTier One.aip b/ext/installfiles/windows/ZeroTier One.aip
index 08ac17ecf..cd016670a 100644
--- a/ext/installfiles/windows/ZeroTier One.aip	
+++ b/ext/installfiles/windows/ZeroTier One.aip	
@@ -24,10 +24,10 @@
     <ROW Property="AiFeatIcoZeroTierOne" Value="ZeroTierIcon.exe" Type="8"/>
     <ROW Property="MSIFASTINSTALL" MultiBuildValue="DefaultBuild:2"/>
     <ROW Property="Manufacturer" Value="ZeroTier, Inc."/>
-    <ROW Property="ProductCode" Value="1033:{B07D0036-6C23-4CFD-9F2A-F2C990225591} " Type="16"/>
+    <ROW Property="ProductCode" Value="1033:{22301716-32F1-4247-8167-3E5441A87A58} " Type="16"/>
     <ROW Property="ProductLanguage" Value="1033"/>
     <ROW Property="ProductName" Value="ZeroTier One"/>
-    <ROW Property="ProductVersion" Value="1.12.0" Options="32"/>
+    <ROW Property="ProductVersion" Value="1.12.1" Options="32"/>
     <ROW Property="REBOOT" MultiBuildValue="DefaultBuild:ReallySuppress"/>
     <ROW Property="SecureCustomProperties" Value="OLDPRODUCTS;AI_NEWERPRODUCTFOUND;AI_SETUPEXEPATH;SETUPEXEDIR"/>
     <ROW Property="UpgradeCode" Value="{B0E2A5F3-88B6-4E77-B922-CB4739B4C4C8}"/>
@@ -62,7 +62,7 @@
     <ROW Directory="regid.201001.com.zerotier_Dir" Directory_Parent="CommonAppDataFolder" DefaultDir="REGID2~1.ZER|regid.2010-01.com.zerotier" DirectoryOptions="12"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiCompsComponent">
-    <ROW Component="AI_CustomARPName" ComponentId="{0C83327B-255D-4E69-A702-EC9414459A68}" Directory_="APPDIR" Attributes="4" KeyPath="DisplayName" Options="1"/>
+    <ROW Component="AI_CustomARPName" ComponentId="{E391FCA4-D005-4309-A481-415FAEB15274}" Directory_="APPDIR" Attributes="4" KeyPath="DisplayName" Options="1"/>
     <ROW Component="AI_DisableModify" ComponentId="{46FFA8C5-A0CB-4E05-9AD3-911D543DE8CA}" Directory_="APPDIR" Attributes="4" KeyPath="NoModify" Options="1"/>
     <ROW Component="AI_ExePath" ComponentId="{8E02B36C-7A19-429B-A93E-77A9261AC918}" Directory_="APPDIR" Attributes="4" KeyPath="AI_ExePath"/>
     <ROW Component="APPDIR" ComponentId="{4DD7907D-D7FE-4CD6-B1A0-B5C1625F5133}" Directory_="APPDIR" Attributes="0"/>
@@ -498,7 +498,7 @@
     <ROW XmlAttribute="xsischemaLocation" XmlElement="swidsoftware_identification_tag" Name="xsi:schemaLocation" Flags="14" Order="3" Value="http://standards.iso.org/iso/19770/-2/2008/schema.xsd software_identification_tag.xsd"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.XmlElementComponent">
-    <ROW XmlElement="swidbuild" ParentElement="swidnumeric" Name="swid:build" Condition="1" Order="2" Flags="14" Text="0" UpdateIndexInParent="0"/>
+    <ROW XmlElement="swidbuild" ParentElement="swidnumeric" Name="swid:build" Condition="1" Order="2" Flags="14" Text="1" UpdateIndexInParent="0"/>
     <ROW XmlElement="swidentitlement_required_indicator" ParentElement="swidsoftware_identification_tag" Name="swid:entitlement_required_indicator" Condition="1" Order="0" Flags="14" Text="false" UpdateIndexInParent="0"/>
     <ROW XmlElement="swidmajor" ParentElement="swidnumeric" Name="swid:major" Condition="1" Order="0" Flags="14" Text="1" UpdateIndexInParent="0"/>
     <ROW XmlElement="swidminor" ParentElement="swidnumeric" Name="swid:minor" Condition="1" Order="1" Flags="14" Text="12" UpdateIndexInParent="0"/>
diff --git a/osdep/Binder.hpp b/osdep/Binder.hpp
index fe08f5195..614c6e725 100644
--- a/osdep/Binder.hpp
+++ b/osdep/Binder.hpp
@@ -87,11 +87,10 @@ namespace ZeroTier {
 class Binder {
   private:
 	struct _Binding {
-		_Binding() : udpSock((PhySocket*)0), tcpListenSock((PhySocket*)0)
+		_Binding() : udpSock((PhySocket*)0)
 		{
 		}
 		PhySocket* udpSock;
-		PhySocket* tcpListenSock;
 		InetAddress address;
 		char ifname[256] = {};
 	};
@@ -111,7 +110,6 @@ class Binder {
 		Mutex::Lock _l(_lock);
 		for (unsigned int b = 0, c = _bindingCount; b < c; ++b) {
 			phy.close(_bindings[b].udpSock, false);
-			phy.close(_bindings[b].tcpListenSock, false);
 		}
 		_bindingCount = 0;
 	}
@@ -133,7 +131,7 @@ class Binder {
 	template <typename PHY_HANDLER_TYPE, typename INTERFACE_CHECKER> void refresh(Phy<PHY_HANDLER_TYPE>& phy, unsigned int* ports, unsigned int portCount, const std::vector<InetAddress> explicitBind, INTERFACE_CHECKER& ifChecker)
 	{
 		std::map<InetAddress, std::string> localIfAddrs;
-		PhySocket *udps, *tcps;
+		PhySocket *udps;
 		Mutex::Lock _l(_lock);
 		bool interfacesEnumerated = true;
 
@@ -419,11 +417,8 @@ class Binder {
 			}
 			else {
 				PhySocket* const udps = _bindings[b].udpSock;
-				PhySocket* const tcps = _bindings[b].tcpListenSock;
 				_bindings[b].udpSock = (PhySocket*)0;
-				_bindings[b].tcpListenSock = (PhySocket*)0;
 				phy.close(udps, false);
-				phy.close(tcps, false);
 			}
 		}
 
@@ -437,24 +432,20 @@ class Binder {
 			}
 			if (bi == _bindingCount) {
 				udps = phy.udpBind(reinterpret_cast<const struct sockaddr*>(&(ii->first)), (void*)0, ZT_UDP_DESIRED_BUF_SIZE);
-				tcps = phy.tcpListen(reinterpret_cast<const struct sockaddr*>(&(ii->first)), (void*)0);
-				if ((udps) && (tcps)) {
+				if (udps) {
 #ifdef __LINUX__
 					// Bind Linux sockets to their device so routes that we manage do not override physical routes (wish all platforms had this!)
 					if (ii->second.length() > 0) {
 						char tmp[256];
 						Utils::scopy(tmp, sizeof(tmp), ii->second.c_str());
 						int fd = (int)Phy<PHY_HANDLER_TYPE>::getDescriptor(udps);
-						if (fd >= 0)
-							setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, tmp, strlen(tmp));
-						fd = (int)Phy<PHY_HANDLER_TYPE>::getDescriptor(tcps);
-						if (fd >= 0)
+						if (fd >= 0) {
 							setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, tmp, strlen(tmp));
+						}
 					}
 #endif	 // __LINUX__
 					if (_bindingCount < ZT_BINDER_MAX_BINDINGS) {
 						_bindings[_bindingCount].udpSock = udps;
-						_bindings[_bindingCount].tcpListenSock = tcps;
 						_bindings[_bindingCount].address = ii->first;
 						memcpy(_bindings[_bindingCount].ifname, (char*)ii->second.c_str(), (int)ii->second.length());
 						++_bindingCount;
@@ -462,7 +453,6 @@ class Binder {
 				}
 				else {
 					phy.close(udps, false);
-					phy.close(tcps, false);
 				}
 			}
 		}
diff --git a/service/OneService.cpp b/service/OneService.cpp
index b5e5c1924..dfa5df056 100644
--- a/service/OneService.cpp
+++ b/service/OneService.cpp
@@ -786,8 +786,11 @@ public:
 	bool _updateAutoApply;
 
     httplib::Server _controlPlane;
+	httplib::Server _controlPlaneV6;
     std::thread _serverThread;
+	std::thread _serverThreadV6;
 	bool _serverThreadRunning;
+	bool _serverThreadRunningV6;
 
     bool _allowTcpFallbackRelay;
 	bool _forceTcpRelay;
@@ -888,8 +891,11 @@ public:
 		,_updater((SoftwareUpdater *)0)
 		,_updateAutoApply(false)
         ,_controlPlane()
+		,_controlPlaneV6()
         ,_serverThread()
+		,_serverThreadV6()
 		,_serverThreadRunning(false)
+		,_serverThreadRunningV6(false)
 		,_forceTcpRelay(false)
 		,_primaryPort(port)
 		,_udpPortPickerCounter(0)
@@ -944,6 +950,10 @@ public:
 		if (_serverThreadRunning) {
 	        _serverThread.join();
 		}
+		_controlPlaneV6.stop();
+		if (_serverThreadRunningV6) {
+			_serverThreadV6.join();
+		}
 
 #ifdef ZT_USE_MINIUPNPC
 		delete _portMapper;
@@ -1527,6 +1537,22 @@ public:
 
     // Internal HTTP Control Plane
     void startHTTPControlPlane() {
+		// control plane endpoints
+		std::string bondShowPath = "/bond/show/([0-9a-fA-F]{10})";
+		std::string bondRotatePath = "/bond/rotate/([0-9a-fA-F]{10})";
+		std::string setBondMtuPath = "/bond/setmtu/([0-9]{3,5})/([a-zA-Z0-9_]{1,16})/([0-9a-fA-F\\.\\:]{1,39})";
+		std::string configPath = "/config";
+		std::string configPostPath = "/config/settings";
+		std::string healthPath = "/health";
+		std::string moonListPath = "/moon";
+		std::string moonPath = "/moon/([0-9a-fA-F]{10})";
+		std::string networkListPath = "/network";
+		std::string networkPath = "/network/([0-9a-fA-F]{16})";
+		std::string peerListPath = "/peer";
+		std::string peerPath = "/peer/([0-9a-fA-F]{10})";
+		std::string statusPath = "/status";
+		std::string metricsPath = "/metrics";
+
         std::vector<std::string> noAuthEndpoints { "/sso", "/health" };
 
 		auto setContent = [=] (const httplib::Request &req, httplib::Response &res, std::string content) {
@@ -1625,8 +1651,7 @@ public:
         };
 
 
-
-		_controlPlane.Get("/bond/show/([0-9a-fA-F]{10})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto bondShow = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			if (!_node->bondController()->inUse()) {
 				setContent(req, res, "");
 				res.status = 400;
@@ -1652,7 +1677,9 @@ public:
 				}
 			}
 			_node->freeQueryResult((void *)pl);
-		});
+		};
+		_controlPlane.Get(bondShowPath, bondShow);
+		_controlPlaneV6.Get(bondShowPath, bondShow);
 
 		auto bondRotate = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			if (!_node->bondController()->inUse()) {
@@ -1675,8 +1702,10 @@ public:
 			}
 			setContent(req, res, "{}");
 		};
-		_controlPlane.Post("/bond/rotate/([0-9a-fA-F]{10})", bondRotate);
-		_controlPlane.Put("/bond/rotate/([0-9a-fA-F]{10})", bondRotate);
+		_controlPlane.Post(bondRotatePath, bondRotate);
+		_controlPlane.Put(bondRotatePath, bondRotate);
+		_controlPlaneV6.Post(bondRotatePath, bondRotate);
+		_controlPlaneV6.Put(bondRotatePath, bondRotate);
 
 		auto setMtu = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			if (!_node->bondController()->inUse()) {
@@ -1688,10 +1717,12 @@ public:
 			res.status = _node->bondController()->setAllMtuByTuple(mtu, req.matches[2].str().c_str(), req.matches[3].str().c_str()) ? 200 : 400;
 			setContent(req, res, "{}");
 		};
-		_controlPlane.Post("/bond/setmtu/([0-9]{3,5})/([a-zA-Z0-9_]{1,16})/([0-9a-fA-F\\.\\:]{1,39})", setMtu);
-		_controlPlane.Put("/bond/setmtu/([0-9]{3,5})/([a-zA-Z0-9_]{1,16})/([0-9a-fA-F\\.\\:]{1,39})", setMtu);
+		_controlPlane.Post(setBondMtuPath, setMtu);
+		_controlPlane.Put(setBondMtuPath, setMtu);
+		_controlPlaneV6.Post(setBondMtuPath, setMtu);
+		_controlPlaneV6.Put(setBondMtuPath, setMtu);
 
-		_controlPlane.Get("/config", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto getConfig = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			std::string config;
 			{
 				Mutex::Lock lc(_localConfig_m);
@@ -1701,7 +1732,9 @@ public:
 				config = "{}";
 			}
 			setContent(req, res, config);
-		});
+		};
+		_controlPlane.Get(configPath, getConfig);
+		_controlPlaneV6.Get(configPath, getConfig);
 
 		auto configPost = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			json j(OSUtils::jsonParse(req.body));
@@ -1718,10 +1751,12 @@ public:
 			}
 			setContent(req, res, "{}");
 		};
-		_controlPlane.Post("/config/settings", configPost);
-		_controlPlane.Put("/config/settings", configPost);
+		_controlPlane.Post(configPostPath, configPost);
+		_controlPlane.Put(configPostPath, configPost);
+		_controlPlaneV6.Post(configPostPath, configPost);
+		_controlPlaneV6.Put(configPostPath, configPost);
 
-		_controlPlane.Get("/health", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto healthGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			json out = json::object();
 
 			char tmp[256];
@@ -1739,9 +1774,11 @@ public:
 			out["clock"] = OSUtils::now();
 
 			setContent(req, res, out.dump());
-		});
+		};
+		_controlPlane.Get(healthPath, healthGet);
+		_controlPlaneV6.Get(healthPath, healthGet);
 
-		_controlPlane.Get("/moon", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto moonListGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			std::vector<World> moons(_node->moons());
 
 			auto out = json::array();
@@ -1751,9 +1788,11 @@ public:
 				out.push_back(mj);
 			}
 			setContent(req, res, out.dump());
-		});
+		};
+		_controlPlane.Get(moonListPath, moonListGet);
+		_controlPlaneV6.Get(moonListPath, moonListGet);
 
-		_controlPlane.Get("/moon/([0-9a-fA-F]{10})", [&, setContent](const httplib::Request &req, httplib::Response &res){
+		auto moonGet = [&, setContent](const httplib::Request &req, httplib::Response &res){
 			std::vector<World> moons(_node->moons());
 			auto input = req.matches[1];
 			auto out = json::object();
@@ -1765,7 +1804,9 @@ public:
 				}
 			}
 			setContent(req, res, out.dump());
-		});
+		};
+		_controlPlane.Get(moonPath, moonGet);
+		_controlPlaneV6.Get(moonPath, moonGet);
 
 		auto moonPost = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			auto input = req.matches[1];
@@ -1804,19 +1845,22 @@ public:
 			}
 			setContent(req, res, out.dump());
 		};
-		_controlPlane.Post("/moon/([0-9a-fA-F]{10})", moonPost);
-		_controlPlane.Put("/moon/([0-9a-fA-F]{10})", moonPost);
+		_controlPlane.Post(moonPath, moonPost);
+		_controlPlane.Put(moonPath, moonPost);
+		_controlPlaneV6.Post(moonPath, moonPost);
+		_controlPlaneV6.Put(moonPath, moonPost);
 
-		_controlPlane.Delete("/moon/([0-9a-fA-F]{10})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto moonDelete = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			auto input = req.matches[1];
 			uint64_t id = Utils::hexStrToU64(input.str().c_str());
 			auto out = json::object();
 			_node->deorbit((void*)0,id);
 			out["result"] = true;
 			setContent(req, res, out.dump());
-		});
+		};
+		_controlPlane.Delete(moonPath, moonDelete);
 
-		_controlPlane.Get("/network", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto networkListGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
             Mutex::Lock _l(_nets_m);
             auto out = json::array();
 
@@ -1827,9 +1871,11 @@ public:
                 out.push_back(nj);
             }
 			setContent(req, res, out.dump());
-        });
+        };
+		_controlPlane.Get(networkListPath, networkListGet);
+		_controlPlaneV6.Get(networkListPath, networkListGet);
 
-        _controlPlane.Get("/network/([0-9a-fA-F]{16})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto networkGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			Mutex::Lock _l(_nets_m);
 
             auto input = req.matches[1];
@@ -1843,7 +1889,9 @@ public:
 			}
 			setContent(req, res, "");
 			res.status = 404;
-        });
+        };
+        _controlPlane.Get(networkPath, networkGet);
+		_controlPlaneV6.Get(networkPath, networkGet);
 
 		auto networkPost = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			auto input = req.matches[1];
@@ -1897,10 +1945,12 @@ public:
 			setContent(req, res, out.dump());
 #endif
 		};
-		_controlPlane.Post("/network/([0-9a-fA-F]{16})", networkPost);
-		_controlPlane.Put("/network/([0-9a-fA-F]){16}", networkPost);
+		_controlPlane.Post(networkPath, networkPost);
+		_controlPlane.Put(networkPath, networkPost);
+		_controlPlaneV6.Post(networkPath, networkPost);
+		_controlPlaneV6.Put(networkPath, networkPost);
 
-		_controlPlane.Delete("/network/([0-9a-fA-F]{16})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto networkDelete = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			auto input = req.matches[1];
 			auto out = json::object();
 			ZT_VirtualNetworkList *nws = _node->networks();
@@ -1913,9 +1963,11 @@ public:
 			}
 			_node->freeQueryResult((void*)nws);
 			setContent(req, res, out.dump());
-		});
+		};
+		_controlPlane.Delete(networkPath, networkDelete);
+		_controlPlaneV6.Delete(networkPath, networkDelete);
 
-		_controlPlane.Get("/peer", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto peerListGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			ZT_PeerList *pl = _node->peers();
 			auto out = nlohmann::json::array();
 
@@ -1931,9 +1983,11 @@ public:
 			}
 			_node->freeQueryResult((void*)pl);
 			setContent(req, res, out.dump());
-		});
+		};
+		_controlPlane.Get(peerListPath, peerListGet);
+		_controlPlaneV6.Get(peerListPath, peerListGet);
 
-		_controlPlane.Get("/peer/([0-9a-fA-F]{10})", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto peerGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
 			ZT_PeerList *pl = _node->peers();
 
 			auto input = req.matches[1];
@@ -1951,9 +2005,11 @@ public:
 			}
 			_node->freeQueryResult((void*)pl);
 			setContent(req, res, out.dump());
-		});
+		};
+		_controlPlane.Get(peerPath, peerGet);
+		_controlPlaneV6.Get(peerPath, peerGet);
 
-		_controlPlane.Get("/status", [&, setContent](const httplib::Request &req, httplib::Response &res) {
+		auto statusGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
             ZT_NodeStatus status;
             _node->status(&status);
 
@@ -2016,10 +2072,13 @@ public:
             out["planetWorldTimestamp"] = planet.timestamp();
 
 			setContent(req, res, out.dump());
-        });
+        };
+		_controlPlane.Get(statusPath, statusGet);
+		_controlPlaneV6.Get(statusPath, statusGet);
 
 #if ZT_SSO_ENABLED
-        _controlPlane.Get("/sso", [this](const httplib::Request &req, httplib::Response &res) {
+		std::string ssoPath = "/sso";
+		auto ssoGet = [this](const httplib::Request &req, httplib::Response &res) {
             std::string htmlTemplatePath = _homePath + ZT_PATH_SEPARATOR + "sso-auth.template.html";
             std::string htmlTemplate;
             if (!OSUtils::readFile(htmlTemplatePath.c_str(), htmlTemplate)) {
@@ -2085,10 +2144,11 @@ public:
 
                 zeroidc::free_cstr(ret);
             }
-        });
+        };
+        _controlPlane.Get(ssoPath, ssoGet);
+		_controlPlaneV6.Get(ssoPath, ssoGet);
 #endif
-
-        _controlPlane.Get("/metrics", [this](const httplib::Request &req, httplib::Response &res) {
+		auto metricsGet = [this](const httplib::Request &req, httplib::Response &res) {
             std::string statspath = _homePath + ZT_PATH_SEPARATOR + "metrics.prom";
             std::string metrics;
             if (OSUtils::readFile(statspath.c_str(), metrics)) {
@@ -2097,9 +2157,11 @@ public:
                 res.set_content("{}", "application/json");
                 res.status = 500;
             }
-        });
+        };
+        _controlPlane.Get(metricsPath, metricsGet);
+		_controlPlaneV6.Get(metricsPath, metricsGet);
 
-		_controlPlane.set_exception_handler([&, setContent](const httplib::Request &req, httplib::Response &res, std::exception_ptr ep) {
+		auto exceptionHandler = [&, setContent](const httplib::Request &req, httplib::Response &res, std::exception_ptr ep) {
 			char buf[1024];
 			auto fmt = "{\"error\": %d, \"description\": \"%s\"}";
 			try {
@@ -2111,39 +2173,70 @@ public:
 			}
 			setContent(req, res, buf);
 			res.status = 500;
-		});
+		};
+		_controlPlane.set_exception_handler(exceptionHandler);
+		_controlPlaneV6.set_exception_handler(exceptionHandler);
 
 		if (_controller) {
-			_controller->configureHTTPControlPlane(_controlPlane, setContent);
+			_controller->configureHTTPControlPlane(_controlPlane, _controlPlaneV6, setContent);
 		}
 
 		_controlPlane.set_pre_routing_handler(authCheck);
+		_controlPlaneV6.set_pre_routing_handler(authCheck);
 
 #if ZT_DEBUG==1
 		_controlPlane.set_logger([](const httplib::Request &req, const httplib::Response &res) {
 			fprintf(stderr, "%s", http_log(req, res).c_str());
 		});
+		_controlPlaneV6.set_logger([](const httplib::Request &req, const httplib::Response &res) {
+			fprintf(stderr, "%s", http_log(req, res).c_str());
+		});
 #endif
 		if (_primaryPort==0) {
 			fprintf(stderr, "unable to determine local control port");
 			exit(-1);
 		}
 
-		if(!_controlPlane.bind_to_port("0.0.0.0", _primaryPort)) {
-			fprintf(stderr, "Error binding control plane to port %d\n", _primaryPort);
-			exit(-1);
+		bool v4controlPlaneBound = false;
+		_controlPlane.set_address_family(AF_INET);
+		if(_controlPlane.bind_to_port("0.0.0.0", _primaryPort)) {
+			_serverThread = std::thread([&] {
+				_serverThreadRunning = true;
+				fprintf(stderr, "Starting Control Plane...\n");
+				if(!_controlPlane.listen_after_bind()) {
+					fprintf(stderr, "Error on listen_after_bind()\n");
+				}
+				fprintf(stderr, "Control Plane Stopped\n");
+				_serverThreadRunning = false;
+			});
+			v4controlPlaneBound = true;
+		} else {
+			fprintf(stderr, "Error binding control plane to 0.0.0.0:%d\n", _primaryPort);
+			v4controlPlaneBound = false;
 		}
 
-        _serverThread = std::thread([&] {
-			_serverThreadRunning = true;
-            fprintf(stderr, "Starting Control Plane...\n");
-            if(!_controlPlane.listen_after_bind()) {
-				fprintf(stderr, "Error on listen_after_bind()\n");
-			}
-            fprintf(stderr, "Control Plane Stopped\n");
-			_serverThreadRunning = false;
-        });
+		bool v6controlPlaneBound = false;
+		_controlPlaneV6.set_address_family(AF_INET6);
+		if(_controlPlaneV6.bind_to_port("::", _primaryPort)) {
+			_serverThreadV6 = std::thread([&] {
+				_serverThreadRunningV6 = true;
+				fprintf(stderr, "Starting V6 Control Plane...\n");
+				if(!_controlPlaneV6.listen_after_bind()) {
+					fprintf(stderr, "Error on V6 listen_after_bind()\n");
+				}
+				fprintf(stderr, "V6 Control Plane Stopped\n");
+				_serverThreadRunningV6 = false;
+			});
+			v6controlPlaneBound = true;
+		} else {
+			fprintf(stderr, "Error binding control plane to [::]:%d\n", _primaryPort);
+			v6controlPlaneBound = false;
+		}
 
+		if (!v4controlPlaneBound && !v6controlPlaneBound) {
+			fprintf(stderr, "ERROR: Could not bind control plane. Exiting...\n");
+			exit(-1);
+		}
     }
 
 	// Must be called after _localConfig is read or modified
diff --git a/version.h b/version.h
index cd4d29061..8c3a6c879 100644
--- a/version.h
+++ b/version.h
@@ -27,7 +27,7 @@
 /**
  * Revision
  */
-#define ZEROTIER_ONE_VERSION_REVISION 0
+#define ZEROTIER_ONE_VERSION_REVISION 1
 
 /**
  * Build version
diff --git a/zerotier-one.spec b/zerotier-one.spec
index dc9547003..a6ae2faf7 100644
--- a/zerotier-one.spec
+++ b/zerotier-one.spec
@@ -1,5 +1,5 @@
 Name:           zerotier-one
-Version:        1.12.0
+Version:        1.12.1
 Release:        1%{?dist}
 Summary:        ZeroTier network virtualization service
 
@@ -143,6 +143,9 @@ chmod 0755 $RPM_BUILD_ROOT/etc/init.d/zerotier-one
 %endif
 
 %changelog
+* Fri Aug 25 2023 Adam Ierymenko <adam.ierymenko@zerotier.com> - 1.12.1
+- see https://github.com/zerotier/ZeroTierOne for release notes
+
 * Thu Aug 17 2023 Adam Ierymenko <adam.ierymenko@zerotier.com> - 1.12.0
 - see https://github.com/zerotier/ZeroTierOne for release notes