From cf02910c307f5da66544a080427aa60179a1ac2e Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Wed, 29 Nov 2023 14:57:49 -0500
Subject: [PATCH 01/46] add a prototype xweb handler for thoughts

---
 common/zac/handler.go           | 86 +++++++++++++++++++++++++++++++++
 controller/controller.go        |  5 ++
 ziti/cmd/edge/quickstart.go     |  6 ---
 ziti/cmd/helpers/env_helpers.go |  4 ++
 4 files changed, 95 insertions(+), 6 deletions(-)
 create mode 100644 common/zac/handler.go

diff --git a/common/zac/handler.go b/common/zac/handler.go
new file mode 100644
index 000000000..0eeb1b776
--- /dev/null
+++ b/common/zac/handler.go
@@ -0,0 +1,86 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package zac
+
+import (
+	gosundheit "github.com/AppsFlyer/go-sundheit"
+	"github.com/openziti/xweb/v2"
+	"net/http"
+	"strings"
+)
+
+const (
+	Binding = "zac"
+)
+
+type ZitiAdminConsoleFactory struct {
+	healthChecker gosundheit.Health
+}
+
+var _ xweb.ApiHandlerFactory = &ZitiAdminConsoleFactory{}
+
+func NewZitiAdminConsoleFactory() *ZitiAdminConsoleFactory {
+	return &ZitiAdminConsoleFactory{}
+}
+
+func (factory ZitiAdminConsoleFactory) Validate(*xweb.InstanceConfig) error {
+	return nil
+}
+
+func (factory ZitiAdminConsoleFactory) Binding() string {
+	return Binding
+}
+
+func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
+	loc := "./"
+	if options["location"] != "" {
+		loc = options["location"].(string)
+	}
+	zac := &ZitiAdminConsoleHandler{
+		httpHandler: http.FileServer(http.Dir(loc)),
+	}
+
+	return zac, nil
+}
+
+type ZitiAdminConsoleHandler struct {
+	options     map[interface{}]interface{}
+	httpHandler http.Handler
+}
+
+func (self *ZitiAdminConsoleHandler) Binding() string {
+	return Binding
+}
+
+func (self *ZitiAdminConsoleHandler) Options() map[interface{}]interface{} {
+	return nil
+}
+
+func (self *ZitiAdminConsoleHandler) RootPath() string {
+	return "/" + Binding
+}
+
+func (self *ZitiAdminConsoleHandler) IsHandler(r *http.Request) bool {
+	return strings.HasPrefix(r.URL.Path, self.RootPath()) || strings.HasPrefix(r.URL.Path, "/assets")
+}
+
+func (self *ZitiAdminConsoleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	if !strings.HasPrefix(request.URL.Path, self.RootPath()) {
+		request.URL.Path = self.RootPath() + "/" + request.URL.Path
+	}
+	self.httpHandler.ServeHTTP(writer, request)
+}
diff --git a/controller/controller.go b/controller/controller.go
index 00931edc9..ccf50b4d7 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -25,6 +25,7 @@ import (
 	"github.com/openziti/transport/v2"
 	"github.com/openziti/ziti/common/capabilities"
 	"github.com/openziti/ziti/common/config"
+	"github.com/openziti/ziti/common/zac"
 	"github.com/openziti/ziti/controller/event"
 	"github.com/openziti/ziti/controller/events"
 	"github.com/openziti/ziti/controller/handler_peer_ctrl"
@@ -247,6 +248,10 @@ func (c *Controller) initWeb() {
 		logrus.WithError(err).Fatalf("failed to create metrics api factory")
 	}
 
+	if err := c.xweb.GetRegistry().Add(zac.NewZitiAdminConsoleFactory()); err != nil {
+		logrus.WithError(err).Fatalf("failed to create myXweb factory")
+	}
+
 }
 
 func (c *Controller) Run() error {
diff --git a/ziti/cmd/edge/quickstart.go b/ziti/cmd/edge/quickstart.go
index b77b3bb91..20084e7b9 100644
--- a/ziti/cmd/edge/quickstart.go
+++ b/ziti/cmd/edge/quickstart.go
@@ -335,18 +335,12 @@ func (o *QuickstartOpts) createMinimalPki() {
 
 		//ziti pki create server --pki-root="${ZITI_HOME}/pki" --ca-name "intermediate-ca" --server-name "server" --server-file "server" --dns "localhost,${ZITI_HOSTNAME}"
 		svr := pki.NewCmdPKICreateServer(o.out, o.errOut)
-		var ips = "127.0.0.1,::1"
-		ip_override := os.Getenv("ZITI_CTRL_EDGE_IP_OVERRIDE")
-		if ip_override != "" {
-			ips = ips + "," + ip_override
-		}
 		svr.SetArgs([]string{
 			fmt.Sprintf("--pki-root=%s", where),
 			fmt.Sprintf("--ca-name=%s", "intermediate-ca"),
 			fmt.Sprintf("--server-name=%s", "server"),
 			fmt.Sprintf("--server-file=%s", "server"),
 			fmt.Sprintf("--dns=%s,%s", "localhost", helpers.GetCtrlAdvertisedAddress()),
-			fmt.Sprintf("--ip=%s", ips),
 		})
 		svrErr := svr.Execute()
 		if svrErr != nil {
diff --git a/ziti/cmd/helpers/env_helpers.go b/ziti/cmd/helpers/env_helpers.go
index 78c90105d..9960b24d2 100644
--- a/ziti/cmd/helpers/env_helpers.go
+++ b/ziti/cmd/helpers/env_helpers.go
@@ -96,6 +96,10 @@ func GetCtrlAdvertisedAddress() string {
 	return getFromEnv(constants.CtrlAdvertisedAddressVarName, HostnameOrNetworkName)
 }
 
+func GetCtrlIpOverride() string {
+	return getFromEnv(constants.CtrlAdvertisedAddressVarName, HostnameOrNetworkName)
+}
+
 func GetEdgeRouterIpOvderride() string {
 	return getFromEnv(constants.ZitiEdgeRouterIPOverrideVarName, defaultValue(""))
 }

From 870517a6783a6ad83217a142f542018a6c8d1bb6 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Wed, 29 Nov 2023 16:26:47 -0500
Subject: [PATCH 02/46] update spa handling

---
 common/zac/handler.go | 54 +++++++++++++++++++++++++++++++++++++------
 1 file changed, 47 insertions(+), 7 deletions(-)

diff --git a/common/zac/handler.go b/common/zac/handler.go
index 0eeb1b776..0bce71264 100644
--- a/common/zac/handler.go
+++ b/common/zac/handler.go
@@ -19,7 +19,10 @@ package zac
 import (
 	gosundheit "github.com/AppsFlyer/go-sundheit"
 	"github.com/openziti/xweb/v2"
+	log "github.com/sirupsen/logrus"
 	"net/http"
+	"os"
+	"path/filepath"
 	"strings"
 )
 
@@ -46,12 +49,16 @@ func (factory ZitiAdminConsoleFactory) Binding() string {
 }
 
 func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
-	loc := "./"
-	if options["location"] != "" {
-		loc = options["location"].(string)
+	loc := options["location"]
+	if loc == nil || loc == "" {
+		log.Panic("location must be supplied in zac options")
+	}
+	indexFile := options["indexFile"]
+	if indexFile == nil || indexFile == "" {
+		indexFile = "index.html"
 	}
 	zac := &ZitiAdminConsoleHandler{
-		httpHandler: http.FileServer(http.Dir(loc)),
+		httpHandler: SpaHandler(loc.(string), "/"+Binding, indexFile.(string)),
 	}
 
 	return zac, nil
@@ -79,8 +86,41 @@ func (self *ZitiAdminConsoleHandler) IsHandler(r *http.Request) bool {
 }
 
 func (self *ZitiAdminConsoleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
-	if !strings.HasPrefix(request.URL.Path, self.RootPath()) {
-		request.URL.Path = self.RootPath() + "/" + request.URL.Path
-	}
 	self.httpHandler.ServeHTTP(writer, request)
 }
+
+// Thanks to https://github.com/roberthodgen/spa-server
+// Serve from a public directory with specific index
+type spaHandler struct {
+	content     string // The directory from which to serve
+	contextRoot string // The context root to remove
+	indexFile   string // The fallback/default file to serve
+}
+
+// Falls back to a supplied index (indexFile) when either condition is true:
+// (1) Request (file) path is not found
+// (2) Request path is a directory
+// Otherwise serves the requested file.
+func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if strings.HasPrefix(r.URL.Path, h.contextRoot) {
+		// strip off the path
+		r.URL.Path = r.URL.Path[len(h.contextRoot):]
+	}
+	p := filepath.Join(h.content, filepath.Clean(r.URL.Path))
+
+	if info, err := os.Stat(p); err != nil {
+		http.ServeFile(w, r, filepath.Join(h.content, h.indexFile))
+		return
+	} else if info.IsDir() {
+		http.ServeFile(w, r, filepath.Join(h.content, h.indexFile))
+		return
+	}
+
+	http.ServeFile(w, r, p)
+}
+
+// Returns a request handler (http.Handler) that serves a single
+// page application from a given public directory (publicDir).
+func SpaHandler(publicDir string, contextRoot string, indexFile string) http.Handler {
+	return &spaHandler{publicDir, contextRoot, indexFile}
+}

From 928ad4ae91c5539942a3b591da36d45e5e55b020 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Wed, 29 Nov 2023 16:49:53 -0500
Subject: [PATCH 03/46] undo extra commits

---
 ziti/cmd/edge/quickstart.go     | 6 ++++++
 ziti/cmd/helpers/env_helpers.go | 4 ----
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/ziti/cmd/edge/quickstart.go b/ziti/cmd/edge/quickstart.go
index 20084e7b9..b77b3bb91 100644
--- a/ziti/cmd/edge/quickstart.go
+++ b/ziti/cmd/edge/quickstart.go
@@ -335,12 +335,18 @@ func (o *QuickstartOpts) createMinimalPki() {
 
 		//ziti pki create server --pki-root="${ZITI_HOME}/pki" --ca-name "intermediate-ca" --server-name "server" --server-file "server" --dns "localhost,${ZITI_HOSTNAME}"
 		svr := pki.NewCmdPKICreateServer(o.out, o.errOut)
+		var ips = "127.0.0.1,::1"
+		ip_override := os.Getenv("ZITI_CTRL_EDGE_IP_OVERRIDE")
+		if ip_override != "" {
+			ips = ips + "," + ip_override
+		}
 		svr.SetArgs([]string{
 			fmt.Sprintf("--pki-root=%s", where),
 			fmt.Sprintf("--ca-name=%s", "intermediate-ca"),
 			fmt.Sprintf("--server-name=%s", "server"),
 			fmt.Sprintf("--server-file=%s", "server"),
 			fmt.Sprintf("--dns=%s,%s", "localhost", helpers.GetCtrlAdvertisedAddress()),
+			fmt.Sprintf("--ip=%s", ips),
 		})
 		svrErr := svr.Execute()
 		if svrErr != nil {
diff --git a/ziti/cmd/helpers/env_helpers.go b/ziti/cmd/helpers/env_helpers.go
index 9960b24d2..78c90105d 100644
--- a/ziti/cmd/helpers/env_helpers.go
+++ b/ziti/cmd/helpers/env_helpers.go
@@ -96,10 +96,6 @@ func GetCtrlAdvertisedAddress() string {
 	return getFromEnv(constants.CtrlAdvertisedAddressVarName, HostnameOrNetworkName)
 }
 
-func GetCtrlIpOverride() string {
-	return getFromEnv(constants.CtrlAdvertisedAddressVarName, HostnameOrNetworkName)
-}
-
 func GetEdgeRouterIpOvderride() string {
 	return getFromEnv(constants.ZitiEdgeRouterIPOverrideVarName, defaultValue(""))
 }

From 9d47580002bdf95e51b210e4f622cb2b1dea7983 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Wed, 29 Nov 2023 16:53:45 -0500
Subject: [PATCH 04/46] make the linter proud

---
 common/zac/handler.go | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/common/zac/handler.go b/common/zac/handler.go
index 0bce71264..f7f93864b 100644
--- a/common/zac/handler.go
+++ b/common/zac/handler.go
@@ -17,7 +17,6 @@
 package zac
 
 import (
-	gosundheit "github.com/AppsFlyer/go-sundheit"
 	"github.com/openziti/xweb/v2"
 	log "github.com/sirupsen/logrus"
 	"net/http"
@@ -31,7 +30,6 @@ const (
 )
 
 type ZitiAdminConsoleFactory struct {
-	healthChecker gosundheit.Health
 }
 
 var _ xweb.ApiHandlerFactory = &ZitiAdminConsoleFactory{}
@@ -65,7 +63,6 @@ func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[int
 }
 
 type ZitiAdminConsoleHandler struct {
-	options     map[interface{}]interface{}
 	httpHandler http.Handler
 }
 
@@ -104,7 +101,7 @@ type spaHandler struct {
 func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if strings.HasPrefix(r.URL.Path, h.contextRoot) {
 		// strip off the path
-		r.URL.Path = r.URL.Path[len(h.contextRoot):]
+		r.URL.Path = strings.TrimPrefix(r.URL.Path, h.contextRoot)
 	}
 	p := filepath.Join(h.content, filepath.Clean(r.URL.Path))
 

From 137fc08054246fd72f26d458a30b843b00546eff Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Wed, 29 Nov 2023 17:14:56 -0500
Subject: [PATCH 05/46] publicDir -> location

---
 common/zac/handler.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/common/zac/handler.go b/common/zac/handler.go
index f7f93864b..435068993 100644
--- a/common/zac/handler.go
+++ b/common/zac/handler.go
@@ -117,7 +117,7 @@ func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 
 // Returns a request handler (http.Handler) that serves a single
-// page application from a given public directory (publicDir).
-func SpaHandler(publicDir string, contextRoot string, indexFile string) http.Handler {
-	return &spaHandler{publicDir, contextRoot, indexFile}
+// page application from a given public directory (location).
+func SpaHandler(location string, contextRoot string, indexFile string) http.Handler {
+	return &spaHandler{location, contextRoot, indexFile}
 }

From f0463d831f463a1d386844cabbe6388bca000efc Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Wed, 29 Nov 2023 17:24:59 -0500
Subject: [PATCH 06/46] forgot to remove the if

---
 common/zac/handler.go | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/common/zac/handler.go b/common/zac/handler.go
index 435068993..af759611a 100644
--- a/common/zac/handler.go
+++ b/common/zac/handler.go
@@ -99,10 +99,7 @@ type spaHandler struct {
 // (2) Request path is a directory
 // Otherwise serves the requested file.
 func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	if strings.HasPrefix(r.URL.Path, h.contextRoot) {
-		// strip off the path
-		r.URL.Path = strings.TrimPrefix(r.URL.Path, h.contextRoot)
-	}
+	r.URL.Path = strings.TrimPrefix(r.URL.Path, h.contextRoot)
 	p := filepath.Join(h.content, filepath.Clean(r.URL.Path))
 
 	if info, err := os.Stat(p); err != nil {

From c7dd1757a3d0abda3a11eb6511970b48c9a87600 Mon Sep 17 00:00:00 2001
From: gberl002 <geoff.berl@netfoundry.io>
Date: Mon, 12 Feb 2024 15:31:17 -0500
Subject: [PATCH 07/46] renamed the handler to a more generic name updated the
 controller config to have a zac binding in place by default

Signed-off-by: gberl002 <geoff.berl@netfoundry.io>
---
 common/{zac => spa_handler}/handler.go         | 18 ++++++++++--------
 controller/controller.go                       |  4 ++--
 .../cmd/create/config_templates/controller.yml |  2 ++
 3 files changed, 14 insertions(+), 10 deletions(-)
 rename common/{zac => spa_handler}/handler.go (86%)

diff --git a/common/zac/handler.go b/common/spa_handler/handler.go
similarity index 86%
rename from common/zac/handler.go
rename to common/spa_handler/handler.go
index af759611a..1f325269e 100644
--- a/common/zac/handler.go
+++ b/common/spa_handler/handler.go
@@ -14,7 +14,7 @@
 	limitations under the License.
 */
 
-package zac
+package spa_handler
 
 import (
 	"github.com/openziti/xweb/v2"
@@ -55,34 +55,34 @@ func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[int
 	if indexFile == nil || indexFile == "" {
 		indexFile = "index.html"
 	}
-	zac := &ZitiAdminConsoleHandler{
+	zac := &SPAHTTPHandler{
 		httpHandler: SpaHandler(loc.(string), "/"+Binding, indexFile.(string)),
 	}
 
 	return zac, nil
 }
 
-type ZitiAdminConsoleHandler struct {
+type SPAHTTPHandler struct {
 	httpHandler http.Handler
 }
 
-func (self *ZitiAdminConsoleHandler) Binding() string {
+func (self *SPAHTTPHandler) Binding() string {
 	return Binding
 }
 
-func (self *ZitiAdminConsoleHandler) Options() map[interface{}]interface{} {
+func (self *SPAHTTPHandler) Options() map[interface{}]interface{} {
 	return nil
 }
 
-func (self *ZitiAdminConsoleHandler) RootPath() string {
+func (self *SPAHTTPHandler) RootPath() string {
 	return "/" + Binding
 }
 
-func (self *ZitiAdminConsoleHandler) IsHandler(r *http.Request) bool {
+func (self *SPAHTTPHandler) IsHandler(r *http.Request) bool {
 	return strings.HasPrefix(r.URL.Path, self.RootPath()) || strings.HasPrefix(r.URL.Path, "/assets")
 }
 
-func (self *ZitiAdminConsoleHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+func (self *SPAHTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
 	self.httpHandler.ServeHTTP(writer, request)
 }
 
@@ -99,8 +99,10 @@ type spaHandler struct {
 // (2) Request path is a directory
 // Otherwise serves the requested file.
 func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	log.Debugf("incoming r.URL.Path: %s", r.URL.Path)
 	r.URL.Path = strings.TrimPrefix(r.URL.Path, h.contextRoot)
 	p := filepath.Join(h.content, filepath.Clean(r.URL.Path))
+	log.Debugf("outgoing r.URL.Path: %s", p)
 
 	if info, err := os.Stat(p); err != nil {
 		http.ServeFile(w, r, filepath.Join(h.content, h.indexFile))
diff --git a/controller/controller.go b/controller/controller.go
index ccf50b4d7..0f11c0f8d 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -25,7 +25,7 @@ import (
 	"github.com/openziti/transport/v2"
 	"github.com/openziti/ziti/common/capabilities"
 	"github.com/openziti/ziti/common/config"
-	"github.com/openziti/ziti/common/zac"
+	"github.com/openziti/ziti/common/spa_handler"
 	"github.com/openziti/ziti/controller/event"
 	"github.com/openziti/ziti/controller/events"
 	"github.com/openziti/ziti/controller/handler_peer_ctrl"
@@ -248,7 +248,7 @@ func (c *Controller) initWeb() {
 		logrus.WithError(err).Fatalf("failed to create metrics api factory")
 	}
 
-	if err := c.xweb.GetRegistry().Add(zac.NewZitiAdminConsoleFactory()); err != nil {
+	if err := c.xweb.GetRegistry().Add(spa_handler.NewZitiAdminConsoleFactory()); err != nil {
 		logrus.WithError(err).Fatalf("failed to create myXweb factory")
 	}
 
diff --git a/ziti/cmd/create/config_templates/controller.yml b/ziti/cmd/create/config_templates/controller.yml
index cd421a677..906a69657 100644
--- a/ziti/cmd/create/config_templates/controller.yml
+++ b/ziti/cmd/create/config_templates/controller.yml
@@ -215,3 +215,5 @@ web:
         options: { }
       - binding: fabric
         options: { }
+      - binding: zac
+        options: { }

From 5e561f7facc6cf98308ea85caaea846c1b5b6158 Mon Sep 17 00:00:00 2001
From: gberl002 <geoff.berl@netfoundry.io>
Date: Tue, 13 Feb 2024 10:42:24 -0500
Subject: [PATCH 08/46] Adding CI support for macos arm Closes #652

Signed-off-by: gberl002 <geoff.berl@netfoundry.io>
---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index daad03c9a..6dd1eebf5 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -56,7 +56,7 @@ jobs:
         run: |
           go install github.com/mitchellh/gox@latest
           $(go env GOPATH)/bin/ziti-ci generate-build-info common/version/info_generated.go version
-          $(go env GOPATH)/bin/gox -cgo -os=darwin -arch=amd64 -output=$GOX_OUTPUT ./...
+          $(go env GOPATH)/bin/gox -cgo -os=darwin -arch=amd64,arm64 -output=$GOX_OUTPUT ./...
 
       - name: Upload artifacts
         uses: actions/upload-artifact@v4

From 17dbbffc55d0e3ee71c792bf4eda8732ad1805c1 Mon Sep 17 00:00:00 2001
From: gberl002 <geoff.berl@netfoundry.io>
Date: Tue, 13 Feb 2024 11:01:50 -0500
Subject: [PATCH 09/46] Moving to a separate line to check for failures

Signed-off-by: gberl002 <geoff.berl@netfoundry.io>
---
 .github/workflows/main.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6dd1eebf5..90cf0eac9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -56,7 +56,8 @@ jobs:
         run: |
           go install github.com/mitchellh/gox@latest
           $(go env GOPATH)/bin/ziti-ci generate-build-info common/version/info_generated.go version
-          $(go env GOPATH)/bin/gox -cgo -os=darwin -arch=amd64,arm64 -output=$GOX_OUTPUT ./...
+          $(go env GOPATH)/bin/gox -cgo -os=darwin -arch=amd64 -output=$GOX_OUTPUT ./...
+          $(go env GOPATH)/bin/gox -cgo -os=darwin -arch=arm64 -output=$GOX_OUTPUT ./...
 
       - name: Upload artifacts
         uses: actions/upload-artifact@v4

From f67e7f0a920b1e2fdc78a5fd0b692ba3200e9626 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 16 Jan 2024 18:00:23 -0500
Subject: [PATCH 10/46] add a test for the all-in-one quickstart

---
 .github/workflows/test-quickstart.yml         | 60 ++++++++++++++-
 quickstart/docker/all-in-one/.env             |  4 +-
 quickstart/docker/all-in-one/Dockerfile       |  4 +-
 quickstart/docker/all-in-one/README.md        | 75 ++++++++++---------
 .../docker/all-in-one/compose.override.yml    | 56 ++++++++++++++
 quickstart/docker/all-in-one/compose.yml      | 28 +++----
 quickstart/docker/docker-compose.yml          |  1 -
 quickstart/docker/image/Dockerfile            |  2 +-
 quickstart/docker/image/run-controller.sh     |  2 +-
 quickstart/docker/image/run-router.sh         |  2 +-
 quickstart/docker/image/ziti-cli-functions.sh |  4 +-
 .../docker/simplified-docker-compose.yml      |  2 +-
 quickstart/test/compose-test.zsh              | 39 +++++-----
 quickstart/test/compose.override.yml          | 15 +++-
 ziti/cmd/edge/.gitignore                      |  1 +
 ziti/cmd/edge/quickstart_automated_test.go    |  1 -
 ziti/cmd/edge/quickstart_shared_test.go       |  5 +-
 17 files changed, 216 insertions(+), 85 deletions(-)
 create mode 100644 quickstart/docker/all-in-one/compose.override.yml
 create mode 100644 ziti/cmd/edge/.gitignore

diff --git a/.github/workflows/test-quickstart.yml b/.github/workflows/test-quickstart.yml
index 9e029a257..9d62a9112 100644
--- a/.github/workflows/test-quickstart.yml
+++ b/.github/workflows/test-quickstart.yml
@@ -19,15 +19,71 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  compose-test:
-    name: Test Compose Quickstart
+  expressInstallTest:
+    name: Test expressInstall() Quickstart
     runs-on: ubuntu-latest
     steps:
       - name: Shallow checkout
         uses: actions/checkout@v3
+
       - name: Install zsh
         shell: bash
         run: sudo apt-get update && sudo apt-get install --yes zsh
+
+      - name: Install Go
+        id: setup-go
+        uses: actions/setup-go@v4
+        with:
+          go-version-file: ./go.mod
+
       - name: Build and run a quickstart container image
         shell: bash
+        env:
+          ZITI_GO_VERSION: ${{ steps.setup-go.outputs.go-version }}
         run: ./quickstart/test/compose-test.zsh
+
+  allInOneTest:
+    name: Test all-in-one Quickstart
+    runs-on: ubuntu-latest
+    env:
+      ZITI_QUICK_TAG: local  # override default :latest since we're not pulling from registry
+      ARTIFACTS_DIR: ${{ github.workspace }}/build  # output dir for `go build`, input dir for `compose up --build`
+      ZIGGY_UID: 1001  # let container EUID run-as GHA "runner" user to share cache, etc.
+    steps:
+      - name: Shallow checkout
+        uses: actions/checkout@v3
+
+      - name: Install Go
+        id: setup-go
+        uses: actions/setup-go@v4
+        with:
+          go-version-file: ./go.mod
+
+      - name: Build ziti executable
+        shell: bash
+        run: |
+          mkdir -pv ${ARTIFACTS_DIR}
+          go build -o ${ARTIFACTS_DIR} ${GITHUB_WORKSPACE}/...
+
+      - name: Run the all-in-one quickstart with locally-built ziti executable
+        shell: bash
+        working-directory: ./quickstart/docker/all-in-one
+        run: docker compose up --build --detach
+
+      - name: Wait for all-in-one quickstart controller then run quickstart tests
+        shell: bash
+        working-directory: ./quickstart/docker/all-in-one
+        env:
+          ZITI_GO_VERSION: ${{ steps.setup-go.outputs.go-version }}
+        run: docker compose --profile test run --rm quickstart-test
+
+      - name: Print debug info
+        if: always()
+        shell: bash
+        working-directory: ./quickstart/docker/all-in-one
+        run: |
+          set +e
+          set -x
+          id runner
+          ls -lAn ${GOCACHE:-${HOME}/.cache/go-build}/ ${GOPATH:-${HOME}/go}/pkg/mod/
+          docker compose --profile test logs
diff --git a/quickstart/docker/all-in-one/.env b/quickstart/docker/all-in-one/.env
index 19cd77223..95355517b 100644
--- a/quickstart/docker/all-in-one/.env
+++ b/quickstart/docker/all-in-one/.env
@@ -1,2 +1,2 @@
-# required until ziti 0.32.0
-ZITI_QUICK_TAG=release-next
\ No newline at end of file
+# optionally set the ziti CLI version to use, e.g., 0.32.0
+ZITI_QUICK_TAG=latest
\ No newline at end of file
diff --git a/quickstart/docker/all-in-one/Dockerfile b/quickstart/docker/all-in-one/Dockerfile
index 423015b44..c627e421d 100644
--- a/quickstart/docker/all-in-one/Dockerfile
+++ b/quickstart/docker/all-in-one/Dockerfile
@@ -1,5 +1,7 @@
 FROM debian:bookworm-slim
 
-COPY ./build/ziti /usr/local/bin/
+ARG ARTIFACTS_DIR=./build
+
+COPY ${ARTIFACTS_DIR}/ziti /usr/local/bin/
 
 CMD ["ziti"]
diff --git a/quickstart/docker/all-in-one/README.md b/quickstart/docker/all-in-one/README.md
index fabf528d6..5f3a42732 100644
--- a/quickstart/docker/all-in-one/README.md
+++ b/quickstart/docker/all-in-one/README.md
@@ -1,13 +1,16 @@
-# minimal Ziti Docker quickstart
+# All-in-one Ziti Docker quickstart
 
-This Docker Compose project runs `ziti edge quickstart` in a container while persisting configs, PKI, database, etc. in the same directory `./persistent/`.
+This Docker Compose project runs `ziti edge quickstart` in a container while persisting configs, PKI, database, etc. in
+a Docker named volume. You may instead persist the state in a filesystem directory on the Docker host by setting env var
+`ZITI_HOME` to the directory's path.
 
 ## Run Ziti
 
 This is the primary use case for this project: running the `ziti edge quickstart` command in the official
 `openziti/ziti-cli` container image.
 
-1. In this "minimal" sub-directory, pull the container images.
+1. In this "all-in-one" sub-directory, pull the container images. This makes the latest official release image available
+   locally.
 
     ```bash
     docker compose pull
@@ -16,13 +19,18 @@ This is the primary use case for this project: running the `ziti edge quickstart
 2. Run the project.
 
     ```bash
-    docker compose up --detach
+    docker compose up
     ```
 
-3. Modify the state in `./persistent/`, and bounce the container.
+3. Modify configuration and bounce the container.
+
+    If you set `ZITI_HOME=./persistent`, then you would modify the configs in `./persistent/` on the Docker host.
+    Otherwise, you would modify the configs in the Docker named volume that's mounted on `/persistent`. For example,
+    `docker compose exec quickstart bash` will get you a shell in the container where you can `cd /persistent` edit the
+    configs with `vi`.
 
     ```bash
-    docker compose up --force-recreate --detach
+    docker compose up --force-recreate
     ```
 
 4. Observe the logs
@@ -50,7 +58,7 @@ This is the primary use case for this project: running the `ziti edge quickstart
 ## Develop Ziti
 
 This is a secondary use case for this Docker Compose project that replaces the `ziti` binary in the container image with
-the one you build locally with `go build` before running the `ziti edge quickstart` command.
+the one you build locally with `go build`.
 
 1. In the top-level directory of the `ziti` project, build the binary.
 
@@ -58,65 +66,64 @@ the one you build locally with `go build` before running the `ziti edge quicksta
     go build -o ./build ./...
     ```
 
-    The build command can also be run from this "minimal" sub-directory.
+    The build command can also be run from this "all-in-one" sub-directory.
 
     ```bash
     go build -o ../../../build ../../../...
     ```
 
-2. In the "minimal" sub-directory, with `Dockerfile` present:
+2. In the "all-in-one" sub-directory, with `Dockerfile` present:
 
     ```bash
-    docker compose up --detach --build
+    ZITI_QUICK_TAG=local docker compose up --build
     ```
 
     By adding this `--build` option to the `up` command, the container image is built from the Dockerfile with your
     locally built `ziti` binary instead of pulling the default `openziti/ziti-cli` container image from Docker Hub. In
-    the `compose.yml`, the Docker build context is defined with environment variable `ZITI_SRC_ROOT` which defaults to
-    `../../../` (three levels up from this directory at the top level of a Git working copy of the source repo).
+    the `compose.yml`, the Docker build context is hard-coded to `../../../` (three levels up from this directory at the
+    top level of a Git working copy of the source repo). Setting `ZITI_QUICK_TAG=local` tags the locally-built container
+    image differently from the official release image's `:latest` tag so you can tell them apart.
 
 ### Troubleshooting
 
 #### Changing File Locations
 
 The Compose project file `compose.yml` and `Dockerfile` have file paths that represent the assumption they're placed in
-a sub-directory three levels deep in a checked-out copy of the `openziti/ziti` source repository. This allows the Dockerfile
-to copy the built binary from the top-level directory `./build`. You can move these files outside the source tree if you
-adjust the paths in both files.
+a sub-directory three levels deep in a checked-out copy of the `openziti/ziti` source repository. This allows the
+Dockerfile to copy the built binary from the top-level directory `./build`. You may set the environment variable
+`ARTIFACTS_DIR` to a different path relative to the build context (top-level directory of the source repo) to change the
+location where the container image build looks for the locally-built `ziti` binary.
 
-#### Building `ziti` in the Dockerfile
+#### Building `ziti` in Docker
 
-If the binary you build on your host doesn't run in the container due to an environment issue, such as a GLIBC version
-mismatch, you have the option to build `ziti` in the container every time you run `up --build`.
+If the binary you build on the Docker host doesn't run in the container due to an environment issue, such as a GLIBC
+version mismatch, you have the option to build `ziti` in the container every time you run
+`ZITI_QUICK_TAG=local docker compose up --build`.
 
-Change `Dockerfile` like this, and run `docker compose up --detach --build` to build the checked-out source tree and run
-the quickstart with the build.
+Change `Dockerfile` like this, and run `ZITI_QUICK_TAG=local docker compose up --build` to build the
+checked-out source tree and run the quickstart with the build.
 
 ```dockerfile
 FROM golang:1.20-bookworm AS builder
+ARG ARTIFACTS_DIR=./build
 WORKDIR /app
 COPY go.mod go.sum ./
 RUN go mod download
 COPY . .
-RUN go build -o ./build/ ./...
+RUN go build -o /app/${ARTIFACTS_DIR} ./...
 
 FROM debian:bookworm-slim
-COPY --from=builder /app/build/ziti /usr/local/bin/
+COPY --from=builder /app/${ARTIFACTS_DIR} /usr/local/bin/
 
 CMD ["ziti"]
 ```
 
-#### Gotcha - Clobbering the Container Image
-
-With `docker compose up --build`, the container image specified in `image` is replaced with the one built from the Dockerfile.
-This clobbers any image you may have pulled from the registry unless you change the value of `image` or comment the line.
+#### Gotcha - Not Clobbering the Downloaded Container Image
 
-```yaml
-    # commenting "image" avoids clobbering the image pulled from the registry
-    # image: ${ZITI_QUICK_IMAGE:-docker.io/openziti/ziti-cli}:${ZITI_QUICK_TAG:-latest}
-    build:
-      context: ${ZITI_SRC_ROOT:-../../../} 
-      dockerfile: ./quickstart/docker/minimal/Dockerfile
-```
+With `docker compose up --build`, the downloaded container image specified in `image` is replaced locally with the one
+built from the Dockerfile.  This clobbers any image you may have pulled from the registry, which can lead to confusion.
+You can prevent this by setting environment variable like `ZITI_QUICK_TAG=local docker compose up --build` to avoid
+clobbering the default `:latest` tag.
 
-Next time you run `docker compose pull` the image from the registry will be refreshed in the local cache.
+If you already clobbered `:latest` just run `ZITI_QUICK_TAG=latest docker compose pull` to refresh your local copy from
+the registry.
diff --git a/quickstart/docker/all-in-one/compose.override.yml b/quickstart/docker/all-in-one/compose.override.yml
new file mode 100644
index 000000000..0855275bf
--- /dev/null
+++ b/quickstart/docker/all-in-one/compose.override.yml
@@ -0,0 +1,56 @@
+services:
+  quickstart-test:
+    profiles:
+      - test
+    depends_on:
+      wait-for-login:
+        condition: service_completed_successfully
+    image: golang:${ZITI_GO_VERSION:-noop}-alpine
+    networks:
+      - quickstart
+    # run as the same user as the host, so we can use the host's GOCACHE
+    user: ${ZIGGY_UID:-1000}
+    volumes:
+      # mount the parent dir of the quickstart, which is the top-level of the ziti repo working copy, as /mnt, so we can
+      # run the tests in the "edge" Go package
+      - ../../../:/mnt
+      # re-run tests if significant changes from last result in GOCACHE
+      - ${GOCACHE:-${HOME}/.cache/go-build}:/.cache/go-build
+      # re-download dep packages if significant changes from last download in GOPATH
+      - ${GOPATH:-${HOME}/go}:/go
+    working_dir: /mnt
+    environment:
+      # verbose, tests tagged 'quickstart && manual', manual means test an existing network, don't run a network inside
+      # the test process
+      GOFLAGS: "-tags=quickstart,manual"
+      GOCACHE: /.cache/go-build
+      GOPATH: /go
+      ZITI_PWD:
+      ZITI_CTRL_EDGE_ADVERTISED_ADDRESS: ${EXTERNAL_DNS:-quickstart}
+      ZITI_ROUTER_ADVERTISED_ADDRESS: ${EXTERNAL_DNS:-quickstart}
+      ZITI_CTRL_EDGE_ADVERTISED_PORT:
+      ZITI_ROUTER_PORT:
+    command: go test -v ./ziti/cmd/edge/...
+      
+  wait-for-login:
+    profiles:
+      - test
+    image: ${ZITI_QUICK_IMAGE:-docker.io/openziti/ziti-cli}:${ZITI_QUICK_TAG:-latest}
+    networks:
+      - quickstart
+    entrypoint:
+      - bash
+      - -euxc
+      - |
+        ATTEMPTS=10
+        DELAY=3
+        until !((ATTEMPTS)) || ziti $${@} &>/dev/null; do
+          (( ATTEMPTS-- ))
+          echo "Waiting for controller to start"
+          sleep $${DELAY}
+        done
+        ziti $${@}
+    command: >
+      -- edge login
+      ${EXTERNAL_DNS:-quickstart}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
+      -u admin -p ${ZITI_PWD:-admin} -y
diff --git a/quickstart/docker/all-in-one/compose.yml b/quickstart/docker/all-in-one/compose.yml
index 81b67186b..a569af181 100644
--- a/quickstart/docker/all-in-one/compose.yml
+++ b/quickstart/docker/all-in-one/compose.yml
@@ -3,9 +3,13 @@ services:
     image: ${ZITI_QUICK_IMAGE:-docker.io/openziti/ziti-cli}:${ZITI_QUICK_TAG:-latest}
     restart: unless-stopped
     build:
-      context: ${ZITI_SRC_ROOT:-../../../} 
+      # the build context is the root of the ziti repo so that BuildKit can access the built ziti executable in /build
+      # and the Dockerfile
+      context: ../../../ 
       dockerfile: ./quickstart/docker/all-in-one/Dockerfile
-      args: {}
+      args:
+        # path of the directory containing the locally-built ziti executable; relative to the build context
+        ARTIFACTS_DIR: ./build
     networks:
       quickstart:
         # this allows other containers to use the same external DNS name to reach the quickstart container from within the
@@ -16,9 +20,9 @@ services:
     - bash
     - -euc
     - |
-      ZITI_CMD+=" --ctrl-address ${EXTERNAL_DNS:-127.0.0.1}"\
+      ZITI_CMD+=" --ctrl-address ${EXTERNAL_DNS:-quickstart}"\
       " --ctrl-port ${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}"\
-      " --router-address ${EXTERNAL_DNS:-127.0.0.1}"\
+      " --router-address ${EXTERNAL_DNS:-quickstart}"\
       " --router-port ${ZITI_ROUTER_PORT:-3022}"\
       " --password ${ZITI_PWD:-admin}"
       echo "DEBUG: run command is: ziti $${@} $${ZITI_CMD}"
@@ -29,11 +33,9 @@ services:
       HOME: /persistent
       PFXLOG_NO_JSON: "${PFXLOG_NO_JSON:-true}"
     volumes:
-    # store the quickstart state in a named volume; "initialize" service's mount must remain aligned to set the owner on
-    # "up"
-    - persistent:/persistent
-    # store the quickstart state on the Docker host in the same directory as this compose.yml file
-    # - ./persistent:/persistent
+    # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
+    # directory, ZITI_HOME 
+    - ${ZITI_HOME:-persistent}:/persistent
     ports:
     - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
     - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
@@ -48,12 +50,10 @@ services:
     user: root
     environment:
       HOME: /persistent
-      # PFXLOG_NO_JSON: "true"
     volumes:
-    # store the quickstart state in a named volume; this mount must align with the "quickstart" service's mount
-    - persistent:/persistent
-    # store the quickstart state on the Docker host in the same directory as this compose.yml file
-    # - ./persistent:/persistent
+    # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
+    # directory, ZITI_HOME 
+    - ${ZITI_HOME:-persistent}:/persistent
 
 # define a custom network so that we can also define a DNS alias for the quickstart container
 networks:
diff --git a/quickstart/docker/docker-compose.yml b/quickstart/docker/docker-compose.yml
index 75eda8236..708990aea 100644
--- a/quickstart/docker/docker-compose.yml
+++ b/quickstart/docker/docker-compose.yml
@@ -1,4 +1,3 @@
-version: '2.4'
 services:
   ziti-controller:
     image: "${ZITI_IMAGE}:${ZITI_VERSION}"
diff --git a/quickstart/docker/image/Dockerfile b/quickstart/docker/image/Dockerfile
index 8003675d5..c4780346d 100644
--- a/quickstart/docker/image/Dockerfile
+++ b/quickstart/docker/image/Dockerfile
@@ -4,7 +4,6 @@ FROM ubuntu:rolling as fetch-ziti-bins
 ARG ZITI_VERSION_OVERRIDE
 ARG DEBIAN_FRONTEND=noninteractive
 
-COPY . /docker.build.context
 RUN apt-get update \
     && apt-get --yes install \
         jq \
@@ -19,6 +18,7 @@ RUN apt-get update \
     #   just build the Dockerfile \
     # to use a specific version of ziti, specify ZITI_VERSION_OVERRIDE then  \
     #   build the Dockerfile
+COPY . /docker.build.context
 RUN bash /docker.build.context/fetch-ziti-bins.sh /ziti-bin
 
 FROM ubuntu:rolling
diff --git a/quickstart/docker/image/run-controller.sh b/quickstart/docker/image/run-controller.sh
index 40faea127..500977fcf 100755
--- a/quickstart/docker/image/run-controller.sh
+++ b/quickstart/docker/image/run-controller.sh
@@ -60,4 +60,4 @@ unset ZITI_PWD
 # create a place for the internal db
 mkdir -p $ZITI_HOME/db
 
-"${ZITI_BIN_DIR}/ziti" controller run "${ZITI_HOME}/${ZITI_CTRL_NAME}.yaml"
+"${ZITI_BIN_DIR}/ziti" controller run ${ZITI_VERBOSE:+--verbose} "${ZITI_HOME}/${ZITI_CTRL_NAME}.yaml"
diff --git a/quickstart/docker/image/run-router.sh b/quickstart/docker/image/run-router.sh
index eb25c4fc1..08059b146 100755
--- a/quickstart/docker/image/run-router.sh
+++ b/quickstart/docker/image/run-router.sh
@@ -72,5 +72,5 @@ fi
 unset ZITI_USER
 unset ZITI_PWD
 
-"${ZITI_BIN_DIR}/ziti" router run "${ZITI_HOME}/${ZITI_ROUTER_NAME}.yaml" > "${ZITI_HOME}/${ZITI_ROUTER_NAME}.log"
+"${ZITI_BIN_DIR}/ziti" router run "${ZITI_HOME}/${ZITI_ROUTER_NAME}.yaml"
 
diff --git a/quickstart/docker/image/ziti-cli-functions.sh b/quickstart/docker/image/ziti-cli-functions.sh
index 3e9e68815..0bd65aa11 100644
--- a/quickstart/docker/image/ziti-cli-functions.sh
+++ b/quickstart/docker/image/ziti-cli-functions.sh
@@ -40,12 +40,12 @@ function _wait_for_controller {
 }
 
 function _wait_for_public_router {
-  local advertised_host_port="${ZITI_ROUTER_NAME}:${ZITI_ROUTER_PORT}"
+  local advertised_host_port="${ZITI_ROUTER_ADVERTISED_ADDRESS}:${ZITI_ROUTER_PORT}"
   local COUNTDOWN=10
   until [[ -s "${ZITI_HOME}/${ZITI_ROUTER_NAME}.cert" ]] \
     && openssl s_client \
       -connect "${advertised_host_port}" \
-      -servername "${ZITI_ROUTER_NAME}" \
+      -servername "${ZITI_ROUTER_ADVERTISED_ADDRESS}" \
       -alpn "ziti-edge,h2,http/1.1" \
       -cert "${ZITI_HOME}/${ZITI_ROUTER_NAME}.cert" \
       -key "${ZITI_HOME}/${ZITI_ROUTER_NAME}.key" \
diff --git a/quickstart/docker/simplified-docker-compose.yml b/quickstart/docker/simplified-docker-compose.yml
index c50429fa0..bb683b669 100644
--- a/quickstart/docker/simplified-docker-compose.yml
+++ b/quickstart/docker/simplified-docker-compose.yml
@@ -2,7 +2,7 @@ services:
   ziti-controller:
     image: "${ZITI_IMAGE}:${ZITI_VERSION}"
     healthcheck:
-      test: curl -m 1 -s -k https://${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}/edge/client/v1/version
+      test: curl -m 1 -s -k -f https://${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}/edge/client/v1/version
       interval: 1s
       timeout: 3s
       retries: 30
diff --git a/quickstart/test/compose-test.zsh b/quickstart/test/compose-test.zsh
index 5884541b9..a597abac1 100755
--- a/quickstart/test/compose-test.zsh
+++ b/quickstart/test/compose-test.zsh
@@ -1,4 +1,4 @@
-#!/usr/bin/env zsh 
+#!/usr/bin/env zsh
 #
 # this script tests the quickstart's ziti-cli-functions.sh, container image creation process, and Compose project by
 # gathering files from a particular GitHub repo ref or a filesystem path and running the quickstart's Go test suite
@@ -39,9 +39,9 @@ TESTDIR="$(mktemp -d -t "${BASENAME%.*}.${DATESTAMP}.XXX")"
 if [[ -z "${ZITI_QUICK_DIR:-}" ]]; then
     ZITI_QUICK_DIR="$(realpath "${DIRNAME}/..")"
 fi
-# if unset, set ZITI_QUICK_IMAGE_TAG to this run's dirname
-if [[ -z "${ZITI_QUICK_IMAGE_TAG:-}" ]]; then
-    ZITI_QUICK_IMAGE_TAG=$(basename "${TESTDIR}")
+# if unset, set ZITI_QUICK_TAG to this run's dirname
+if [[ -z "${ZITI_QUICK_TAG:-}" ]]; then
+    ZITI_QUICK_TAG=$(basename "${TESTDIR}")
 fi
 
 # case "${1:-}" in
@@ -68,23 +68,23 @@ if [[ -n "${ZITI_QUICK_DIR:-}" ]]; then
     for FILE in "${QUICK_FILES[@]}"; do
         cp "${ZITI_QUICK_DIR}/${FILE}" .
     done
-    if [[ -n "${ZITI_QUICK_IMAGE_TAG:-}" ]]; then
+    if [[ -n "${ZITI_QUICK_TAG:-}" ]]; then
         if [[ -x "${ZITI_QUICK_DIR:-}/docker/createLocalImage.sh" ]]; then
             (
                 cd "${ZITI_QUICK_DIR}/docker"
                 unset ZITI_VERSION ZITI_OVERRIDE_VERSION  # always build the local source
-                ./createLocalImage.sh --build "${ZITI_QUICK_IMAGE_TAG}"
+                ./createLocalImage.sh --build "${ZITI_QUICK_TAG}"
             )
         else
-            echo "ERROR: ZITI_QUICK_IMAGE_TAG is set but ZITI_QUICK_DIR/docker/createLocalImage.sh is not executable" >&2
+            echo "ERROR: ZITI_QUICK_TAG is set but ZITI_QUICK_DIR/docker/createLocalImage.sh is not executable" >&2
             exit 1
         fi
     fi
-elif [[ -n "${ZITI_QUICK_IMAGE_TAG:-}" ]]; then
-    echo "ERROR: ZITI_QUICK_IMAGE_TAG is set but ZITI_QUICK_DIR is not set" >&2
+elif [[ -n "${ZITI_QUICK_TAG:-}" ]]; then
+    echo "ERROR: ZITI_QUICK_TAG is set but ZITI_QUICK_DIR is not set" >&2
     exit 1
 else
-    echo "ERROR: ZITI_QUICK_IMAGE_TAG is not set, try running with --local" >&2
+    echo "ERROR: ZITI_QUICK_TAG is not set, try running with --local" >&2
     exit 1
 fi
 
@@ -92,11 +92,13 @@ fi
 mv ./simplified-docker-compose.yml ./compose.yml
 
 # learn the expected Go version from the Go mod file so we can pull the correct container image
-ZITI_GO_VERSION="$(awk '/^go[[:space:]]+/ {print $2}' ./go.mod)"
+: ${ZITI_GO_VERSION:="$(awk '/^go[[:space:]]+/ {print $2}' ./go.mod)"}
 # make this var available in the Compose project
 sed -E \
-    -e  "s/^(#[[:space:]]+)?(ZITI_PWD)=.*/\2=${ZITI_PWD}/" \
-    -e  "s/^(#[[:space:]]+)?(ZITI_INTERFACE)=.*/\2=${ZITI_INTERFACE:-127.0.0.1}/" ./.env > ./.env.tmp
+    -e  "s/^(#[[:space:]]*)?(ZITI_PWD)=.*/\2=${ZITI_PWD}/" \
+    -e  "s/^(#[[:space:]]*)?(ZITI_ROUTER_NAME)=.*/\2=${ZITI_ROUTER_NAME:=quickstart-router}/" \
+    -e  "s/^(#[[:space:]]*)?(ZITI_ROUTER_ADVERTISED_ADDRESS)=.*/\2=${ZITI_ROUTER_ADVERTISED_ADDRESS:=ziti-edge-router}/" \
+    -e  "s/^(#[[:space:]]*)?(ZITI_INTERFACE)=.*/\2=${ZITI_INTERFACE:-127.0.0.1}/" ./.env > ./.env.tmp
 mv ./.env.tmp ./.env
 
 # pull images preemptively that we never build locally because pull=never when using a local quickstart image
@@ -116,13 +118,13 @@ echo -e "ZITI_GO_VERSION=${ZITI_GO_VERSION}"\
         "\nZITI_QUICK_DIR=${ZITI_QUICK_DIR}" \
         >> ./.env
 
-# if ZITI_QUICK_IMAGE_TAG is set then run the locally-built image
-if [[ -n "${ZITI_QUICK_IMAGE_TAG:-}" ]]; then
-    sed -Ee "s/^(#[[:space:]]+)?(ZITI_VERSION)=.*/\2=${ZITI_QUICK_IMAGE_TAG}/" ./.env > ./.env.tmp
+# if ZITI_QUICK_TAG is set then run the locally-built image
+if [[ -n "${ZITI_QUICK_TAG:-}" ]]; then
+    sed -Ee "s/^(#[[:space:]]*)?(ZITI_VERSION)=.*/\2=${ZITI_QUICK_TAG}/" ./.env > ./.env.tmp
     mv ./.env.tmp ./.env
     docker compose up --detach --pull=never &>/dev/null # no pull because local quickstart image
 else
-    echo "ERROR: ZITI_QUICK_IMAGE_TAG is not set" >&2
+    echo "ERROR: ZITI_QUICK_TAG is not set" >&2
     exit 1
 fi
 
@@ -153,7 +155,6 @@ docker compose exec ziti-controller \
     '
         # TODO: re-add cert checks to above test suite after https://github.com/openziti/ziti/pull/1278
         # zsh /persistent/check-cert-chains.zsh;
-docker compose run quickstart-test
+docker compose --profile test run --rm quickstart-test
 
 echo -e "\nINFO: Test completed successfully."
-
diff --git a/quickstart/test/compose.override.yml b/quickstart/test/compose.override.yml
index bd3846188..ef0822d8d 100644
--- a/quickstart/test/compose.override.yml
+++ b/quickstart/test/compose.override.yml
@@ -1,14 +1,21 @@
 services:
   quickstart-test:
+    profiles:
+      - test
     image: golang:${ZITI_GO_VERSION:-noop}-alpine
     volumes:
       # mount the parent dir of the quickstart, which is the top-level of the ziti repo working copy, as /mnt
       - ${ZITI_QUICK_DIR:-noop}/..:/mnt
-      - ${GOPATH:-noop}:/go
+      # re-run tests if significant changes from last result in GOCACHE
+      - ${GOCACHE:-${HOME}/.cache/go-build}:/.cache/go-build
+      # re-download dep packages if significant changes from last download in GOPATH
+      - ${GOPATH:-${HOME}/go}:/go
     working_dir: /mnt
     environment:
-      - ZITI_PWD
+      GOCACHE: /.cache/go-build
+      GOPATH: /go
+      GOFLAGS: "-tags=quickstart,manual"
+      ZITI_PWD:
     networks:
       - ziti
-    command: >
-      go test -v -tags "quickstart manual" ./ziti/cmd/edge/...
+    command: go test -v ./ziti/cmd/edge/...
diff --git a/ziti/cmd/edge/.gitignore b/ziti/cmd/edge/.gitignore
new file mode 100644
index 000000000..fd08e800f
--- /dev/null
+++ b/ziti/cmd/edge/.gitignore
@@ -0,0 +1 @@
+/gotester.json
diff --git a/ziti/cmd/edge/quickstart_automated_test.go b/ziti/cmd/edge/quickstart_automated_test.go
index f41a770bf..2e087211b 100644
--- a/ziti/cmd/edge/quickstart_automated_test.go
+++ b/ziti/cmd/edge/quickstart_automated_test.go
@@ -15,7 +15,6 @@ import (
 func TestEdgeQuickstartAutomated(t *testing.T) {
 	ctx, cancel := context.WithCancel(context.Background())
 	_ = os.Setenv("ZITI_CTRL_EDGE_ADVERTISED_ADDRESS", "localhost") //force localhost
-	_ = os.Setenv("ZITI_ROUTER_NAME", "quickstart-router")
 	cmdComplete := make(chan bool)
 	qs := NewQuickStartCmd(os.Stdout, os.Stderr, ctx)
 	go func() {
diff --git a/ziti/cmd/edge/quickstart_shared_test.go b/ziti/cmd/edge/quickstart_shared_test.go
index 0b83a7646..62a753c02 100644
--- a/ziti/cmd/edge/quickstart_shared_test.go
+++ b/ziti/cmd/edge/quickstart_shared_test.go
@@ -385,9 +385,12 @@ func performQuickstartTest(t *testing.T) {
 	if advPort == "" {
 		advPort = "1280"
 	}
+	// friendly name of the edge router entity in the controller (not a domain name, not an address)
 	erName := os.Getenv("ZITI_ROUTER_NAME")
 	if erName == "" {
-		erName = "ziti-edge-router"
+		// this default value is set to match the default value used by "go test" --tags="quickstart automated"
+		// and the default value of the router entity name used by the "ziti edge quickstart" command
+		erName = "quickstart-router"
 	}
 
 	ctrlAddress := "https://" + advAddy + ":" + advPort

From 861b61cb7cea9ec95e75f93f9e2dd306adb8d223 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 13 Feb 2024 15:23:42 -0500
Subject: [PATCH 11/46] require valid controller cert to login

---
 .../docker/all-in-one/compose.override.yml    |  5 ++-
 quickstart/docker/all-in-one/compose.yml      | 36 +++++++++----------
 2 files changed, 22 insertions(+), 19 deletions(-)

diff --git a/quickstart/docker/all-in-one/compose.override.yml b/quickstart/docker/all-in-one/compose.override.yml
index 0855275bf..0d8e171cd 100644
--- a/quickstart/docker/all-in-one/compose.override.yml
+++ b/quickstart/docker/all-in-one/compose.override.yml
@@ -38,6 +38,8 @@ services:
     image: ${ZITI_QUICK_IMAGE:-docker.io/openziti/ziti-cli}:${ZITI_QUICK_TAG:-latest}
     networks:
       - quickstart
+    volumes:
+      - ${ZITI_HOME:-persistent}:/persistent
     entrypoint:
       - bash
       - -euxc
@@ -53,4 +55,5 @@ services:
     command: >
       -- edge login
       ${EXTERNAL_DNS:-quickstart}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
-      -u admin -p ${ZITI_PWD:-admin} -y
+      -u ${ZITI_USER:-admin} -p ${ZITI_PWD:-admin}
+      --ca /persistent/pki/root-ca/certs/root-ca.cert
diff --git a/quickstart/docker/all-in-one/compose.yml b/quickstart/docker/all-in-one/compose.yml
index a569af181..5ea648c34 100644
--- a/quickstart/docker/all-in-one/compose.yml
+++ b/quickstart/docker/all-in-one/compose.yml
@@ -17,28 +17,28 @@ services:
         aliases:
           - ${EXTERNAL_DNS:-null}
     entrypoint:
-    - bash
-    - -euc
-    - |
-      ZITI_CMD+=" --ctrl-address ${EXTERNAL_DNS:-quickstart}"\
-      " --ctrl-port ${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}"\
-      " --router-address ${EXTERNAL_DNS:-quickstart}"\
-      " --router-port ${ZITI_ROUTER_PORT:-3022}"\
-      " --password ${ZITI_PWD:-admin}"
-      echo "DEBUG: run command is: ziti $${@} $${ZITI_CMD}"
-      exec ziti "$${@}" $${ZITI_CMD}
+      - bash
+      - -euc
+      - |
+        ZITI_CMD+=" --ctrl-address ${EXTERNAL_DNS:-quickstart}"\
+        " --ctrl-port ${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}"\
+        " --router-address ${EXTERNAL_DNS:-quickstart}"\
+        " --router-port ${ZITI_ROUTER_PORT:-3022}"\
+        " --password ${ZITI_PWD:-admin}"
+        echo "DEBUG: run command is: ziti $${@} $${ZITI_CMD}"
+        exec ziti "$${@}" $${ZITI_CMD}
     command: -- edge quickstart --home /persistent
     user: ${ZIGGY_UID:-1000}
     environment:
       HOME: /persistent
       PFXLOG_NO_JSON: "${PFXLOG_NO_JSON:-true}"
     volumes:
-    # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
-    # directory, ZITI_HOME 
-    - ${ZITI_HOME:-persistent}:/persistent
+      # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
+      # directory, ZITI_HOME 
+      - ${ZITI_HOME:-persistent}:/persistent
     ports:
-    - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
-    - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
+      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
+      - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
     depends_on:
       initialize:
         condition: service_completed_successfully
@@ -51,9 +51,9 @@ services:
     environment:
       HOME: /persistent
     volumes:
-    # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
-    # directory, ZITI_HOME 
-    - ${ZITI_HOME:-persistent}:/persistent
+      # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
+      # directory, ZITI_HOME 
+      - ${ZITI_HOME:-persistent}:/persistent
 
 # define a custom network so that we can also define a DNS alias for the quickstart container
 networks:

From 164c075e2676e86703b40d8e3314bfbdb5fe9c8d Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 13 Feb 2024 15:58:15 -0500
Subject: [PATCH 12/46] don't change the default entity of the router created
 by the quickstart test, just use the env var

---
 quickstart/test/compose.override.yml    | 1 +
 ziti/cmd/edge/quickstart_shared_test.go | 5 +----
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/quickstart/test/compose.override.yml b/quickstart/test/compose.override.yml
index ef0822d8d..88a76657c 100644
--- a/quickstart/test/compose.override.yml
+++ b/quickstart/test/compose.override.yml
@@ -15,6 +15,7 @@ services:
       GOCACHE: /.cache/go-build
       GOPATH: /go
       GOFLAGS: "-tags=quickstart,manual"
+      ZITI_ROUTER_NAME:
       ZITI_PWD:
     networks:
       - ziti
diff --git a/ziti/cmd/edge/quickstart_shared_test.go b/ziti/cmd/edge/quickstart_shared_test.go
index 62a753c02..0b83a7646 100644
--- a/ziti/cmd/edge/quickstart_shared_test.go
+++ b/ziti/cmd/edge/quickstart_shared_test.go
@@ -385,12 +385,9 @@ func performQuickstartTest(t *testing.T) {
 	if advPort == "" {
 		advPort = "1280"
 	}
-	// friendly name of the edge router entity in the controller (not a domain name, not an address)
 	erName := os.Getenv("ZITI_ROUTER_NAME")
 	if erName == "" {
-		// this default value is set to match the default value used by "go test" --tags="quickstart automated"
-		// and the default value of the router entity name used by the "ziti edge quickstart" command
-		erName = "quickstart-router"
+		erName = "ziti-edge-router"
 	}
 
 	ctrlAddress := "https://" + advAddy + ":" + advPort

From e8766dd27691ee2f0e1ed27c752d4111cb546b73 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 13 Feb 2024 16:10:06 -0500
Subject: [PATCH 13/46] set the router entity name for the all-in-one qs test

---
 quickstart/docker/all-in-one/compose.override.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/quickstart/docker/all-in-one/compose.override.yml b/quickstart/docker/all-in-one/compose.override.yml
index 0d8e171cd..9766a164c 100644
--- a/quickstart/docker/all-in-one/compose.override.yml
+++ b/quickstart/docker/all-in-one/compose.override.yml
@@ -29,6 +29,7 @@ services:
       ZITI_CTRL_EDGE_ADVERTISED_ADDRESS: ${EXTERNAL_DNS:-quickstart}
       ZITI_ROUTER_ADVERTISED_ADDRESS: ${EXTERNAL_DNS:-quickstart}
       ZITI_CTRL_EDGE_ADVERTISED_PORT:
+      ZITI_ROUTER_NAME: ${ZITI_ROUTER_NAME:-quickstart-router}
       ZITI_ROUTER_PORT:
     command: go test -v ./ziti/cmd/edge/...
       

From 14f3e9280644ab165aeb6fb3f1a9d5d3be9623b6 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 13 Feb 2024 16:13:49 -0500
Subject: [PATCH 14/46] tweak the expressInstall quickstart

---
 quickstart/docker/docker-compose.yml            | 1 -
 quickstart/docker/image/Dockerfile              | 2 +-
 quickstart/docker/image/run-controller.sh       | 2 +-
 quickstart/docker/image/run-router.sh           | 2 +-
 quickstart/docker/simplified-docker-compose.yml | 2 +-
 ziti/cmd/edge/.gitignore                        | 1 +
 6 files changed, 5 insertions(+), 5 deletions(-)
 create mode 100644 ziti/cmd/edge/.gitignore

diff --git a/quickstart/docker/docker-compose.yml b/quickstart/docker/docker-compose.yml
index 75eda8236..708990aea 100644
--- a/quickstart/docker/docker-compose.yml
+++ b/quickstart/docker/docker-compose.yml
@@ -1,4 +1,3 @@
-version: '2.4'
 services:
   ziti-controller:
     image: "${ZITI_IMAGE}:${ZITI_VERSION}"
diff --git a/quickstart/docker/image/Dockerfile b/quickstart/docker/image/Dockerfile
index 8003675d5..c4780346d 100644
--- a/quickstart/docker/image/Dockerfile
+++ b/quickstart/docker/image/Dockerfile
@@ -4,7 +4,6 @@ FROM ubuntu:rolling as fetch-ziti-bins
 ARG ZITI_VERSION_OVERRIDE
 ARG DEBIAN_FRONTEND=noninteractive
 
-COPY . /docker.build.context
 RUN apt-get update \
     && apt-get --yes install \
         jq \
@@ -19,6 +18,7 @@ RUN apt-get update \
     #   just build the Dockerfile \
     # to use a specific version of ziti, specify ZITI_VERSION_OVERRIDE then  \
     #   build the Dockerfile
+COPY . /docker.build.context
 RUN bash /docker.build.context/fetch-ziti-bins.sh /ziti-bin
 
 FROM ubuntu:rolling
diff --git a/quickstart/docker/image/run-controller.sh b/quickstart/docker/image/run-controller.sh
index 40faea127..500977fcf 100755
--- a/quickstart/docker/image/run-controller.sh
+++ b/quickstart/docker/image/run-controller.sh
@@ -60,4 +60,4 @@ unset ZITI_PWD
 # create a place for the internal db
 mkdir -p $ZITI_HOME/db
 
-"${ZITI_BIN_DIR}/ziti" controller run "${ZITI_HOME}/${ZITI_CTRL_NAME}.yaml"
+"${ZITI_BIN_DIR}/ziti" controller run ${ZITI_VERBOSE:+--verbose} "${ZITI_HOME}/${ZITI_CTRL_NAME}.yaml"
diff --git a/quickstart/docker/image/run-router.sh b/quickstart/docker/image/run-router.sh
index eb25c4fc1..08059b146 100755
--- a/quickstart/docker/image/run-router.sh
+++ b/quickstart/docker/image/run-router.sh
@@ -72,5 +72,5 @@ fi
 unset ZITI_USER
 unset ZITI_PWD
 
-"${ZITI_BIN_DIR}/ziti" router run "${ZITI_HOME}/${ZITI_ROUTER_NAME}.yaml" > "${ZITI_HOME}/${ZITI_ROUTER_NAME}.log"
+"${ZITI_BIN_DIR}/ziti" router run "${ZITI_HOME}/${ZITI_ROUTER_NAME}.yaml"
 
diff --git a/quickstart/docker/simplified-docker-compose.yml b/quickstart/docker/simplified-docker-compose.yml
index c50429fa0..bb683b669 100644
--- a/quickstart/docker/simplified-docker-compose.yml
+++ b/quickstart/docker/simplified-docker-compose.yml
@@ -2,7 +2,7 @@ services:
   ziti-controller:
     image: "${ZITI_IMAGE}:${ZITI_VERSION}"
     healthcheck:
-      test: curl -m 1 -s -k https://${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}/edge/client/v1/version
+      test: curl -m 1 -s -k -f https://${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}/edge/client/v1/version
       interval: 1s
       timeout: 3s
       retries: 30
diff --git a/ziti/cmd/edge/.gitignore b/ziti/cmd/edge/.gitignore
new file mode 100644
index 000000000..fd08e800f
--- /dev/null
+++ b/ziti/cmd/edge/.gitignore
@@ -0,0 +1 @@
+/gotester.json

From 207ffd25935221f3bdb2567fa0c80c2608a3e7a6 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 13 Feb 2024 16:16:38 -0500
Subject: [PATCH 15/46] minimize review delta by reverting changes not strictly
 necessary for this branch to pass tests

---
 quickstart/docker/docker-compose.yml            | 1 +
 quickstart/docker/image/Dockerfile              | 2 +-
 quickstart/docker/image/run-controller.sh       | 2 +-
 quickstart/docker/image/run-router.sh           | 2 +-
 quickstart/docker/simplified-docker-compose.yml | 2 +-
 ziti/cmd/edge/.gitignore                        | 1 -
 6 files changed, 5 insertions(+), 5 deletions(-)
 delete mode 100644 ziti/cmd/edge/.gitignore

diff --git a/quickstart/docker/docker-compose.yml b/quickstart/docker/docker-compose.yml
index 708990aea..75eda8236 100644
--- a/quickstart/docker/docker-compose.yml
+++ b/quickstart/docker/docker-compose.yml
@@ -1,3 +1,4 @@
+version: '2.4'
 services:
   ziti-controller:
     image: "${ZITI_IMAGE}:${ZITI_VERSION}"
diff --git a/quickstart/docker/image/Dockerfile b/quickstart/docker/image/Dockerfile
index c4780346d..8003675d5 100644
--- a/quickstart/docker/image/Dockerfile
+++ b/quickstart/docker/image/Dockerfile
@@ -4,6 +4,7 @@ FROM ubuntu:rolling as fetch-ziti-bins
 ARG ZITI_VERSION_OVERRIDE
 ARG DEBIAN_FRONTEND=noninteractive
 
+COPY . /docker.build.context
 RUN apt-get update \
     && apt-get --yes install \
         jq \
@@ -18,7 +19,6 @@ RUN apt-get update \
     #   just build the Dockerfile \
     # to use a specific version of ziti, specify ZITI_VERSION_OVERRIDE then  \
     #   build the Dockerfile
-COPY . /docker.build.context
 RUN bash /docker.build.context/fetch-ziti-bins.sh /ziti-bin
 
 FROM ubuntu:rolling
diff --git a/quickstart/docker/image/run-controller.sh b/quickstart/docker/image/run-controller.sh
index 500977fcf..40faea127 100755
--- a/quickstart/docker/image/run-controller.sh
+++ b/quickstart/docker/image/run-controller.sh
@@ -60,4 +60,4 @@ unset ZITI_PWD
 # create a place for the internal db
 mkdir -p $ZITI_HOME/db
 
-"${ZITI_BIN_DIR}/ziti" controller run ${ZITI_VERBOSE:+--verbose} "${ZITI_HOME}/${ZITI_CTRL_NAME}.yaml"
+"${ZITI_BIN_DIR}/ziti" controller run "${ZITI_HOME}/${ZITI_CTRL_NAME}.yaml"
diff --git a/quickstart/docker/image/run-router.sh b/quickstart/docker/image/run-router.sh
index 08059b146..eb25c4fc1 100755
--- a/quickstart/docker/image/run-router.sh
+++ b/quickstart/docker/image/run-router.sh
@@ -72,5 +72,5 @@ fi
 unset ZITI_USER
 unset ZITI_PWD
 
-"${ZITI_BIN_DIR}/ziti" router run "${ZITI_HOME}/${ZITI_ROUTER_NAME}.yaml"
+"${ZITI_BIN_DIR}/ziti" router run "${ZITI_HOME}/${ZITI_ROUTER_NAME}.yaml" > "${ZITI_HOME}/${ZITI_ROUTER_NAME}.log"
 
diff --git a/quickstart/docker/simplified-docker-compose.yml b/quickstart/docker/simplified-docker-compose.yml
index bb683b669..c50429fa0 100644
--- a/quickstart/docker/simplified-docker-compose.yml
+++ b/quickstart/docker/simplified-docker-compose.yml
@@ -2,7 +2,7 @@ services:
   ziti-controller:
     image: "${ZITI_IMAGE}:${ZITI_VERSION}"
     healthcheck:
-      test: curl -m 1 -s -k -f https://${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}/edge/client/v1/version
+      test: curl -m 1 -s -k https://${ZITI_CTRL_EDGE_ADVERTISED_ADDRESS:-ziti-edge-controller}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}/edge/client/v1/version
       interval: 1s
       timeout: 3s
       retries: 30
diff --git a/ziti/cmd/edge/.gitignore b/ziti/cmd/edge/.gitignore
deleted file mode 100644
index fd08e800f..000000000
--- a/ziti/cmd/edge/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/gotester.json

From 186c28c3870aca85e6192b6898c48005610bf7f6 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 13 Feb 2024 18:48:50 -0500
Subject: [PATCH 16/46] restore the ZITI_ROUTER_NAME env var to the automated
 test

---
 ziti/cmd/edge/quickstart_automated_test.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ziti/cmd/edge/quickstart_automated_test.go b/ziti/cmd/edge/quickstart_automated_test.go
index 2e087211b..f41a770bf 100644
--- a/ziti/cmd/edge/quickstart_automated_test.go
+++ b/ziti/cmd/edge/quickstart_automated_test.go
@@ -15,6 +15,7 @@ import (
 func TestEdgeQuickstartAutomated(t *testing.T) {
 	ctx, cancel := context.WithCancel(context.Background())
 	_ = os.Setenv("ZITI_CTRL_EDGE_ADVERTISED_ADDRESS", "localhost") //force localhost
+	_ = os.Setenv("ZITI_ROUTER_NAME", "quickstart-router")
 	cmdComplete := make(chan bool)
 	qs := NewQuickStartCmd(os.Stdout, os.Stderr, ctx)
 	go func() {

From a3285b75c259e82c913287a2111451de50f08735 Mon Sep 17 00:00:00 2001
From: gberl002 <geoff.berl@netfoundry.io>
Date: Wed, 14 Feb 2024 13:29:33 -0500
Subject: [PATCH 17/46] Updating the changelog

Signed-off-by: gberl002 <geoff.berl@netfoundry.io>
---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4763972e8..358d48d86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
     * [Issue #1734](https://github.com/openziti/ziti/issues/1734) - Make API rate limiter enabled by default
     * [Issue #1726](https://github.com/openziti/ziti/issues/1726) - Fix some sdk hosting logging
     * [Issue #1725](https://github.com/openziti/ziti/issues/1725) - Fix panic in entity event processing
+    * [Issue #652](https://github.com/openziti/ziti/issues/652) - CI support for MacOS arm64
 
 # Release 0.32.1
 

From 0a27305ba64c3ee1c67719fbf90f43a2797df06c Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Thu, 15 Feb 2024 15:38:33 -0500
Subject: [PATCH 18/46] refine all-in-one quickstart and test

---
 .github/workflows/test-quickstart.yml         |  8 --------
 quickstart/docker/all-in-one/Dockerfile       |  1 +
 quickstart/docker/all-in-one/README.md        | 11 ++++------
 .../docker/all-in-one/compose.override.yml    |  5 +++--
 quickstart/docker/all-in-one/compose.yml      | 20 +++++++++----------
 quickstart/test/compose-test.zsh              | 10 ++++++++--
 6 files changed, 26 insertions(+), 29 deletions(-)

diff --git a/.github/workflows/test-quickstart.yml b/.github/workflows/test-quickstart.yml
index 9d62a9112..8624d223f 100644
--- a/.github/workflows/test-quickstart.yml
+++ b/.github/workflows/test-quickstart.yml
@@ -30,16 +30,8 @@ jobs:
         shell: bash
         run: sudo apt-get update && sudo apt-get install --yes zsh
 
-      - name: Install Go
-        id: setup-go
-        uses: actions/setup-go@v4
-        with:
-          go-version-file: ./go.mod
-
       - name: Build and run a quickstart container image
         shell: bash
-        env:
-          ZITI_GO_VERSION: ${{ steps.setup-go.outputs.go-version }}
         run: ./quickstart/test/compose-test.zsh
 
   allInOneTest:
diff --git a/quickstart/docker/all-in-one/Dockerfile b/quickstart/docker/all-in-one/Dockerfile
index c627e421d..26badf53b 100644
--- a/quickstart/docker/all-in-one/Dockerfile
+++ b/quickstart/docker/all-in-one/Dockerfile
@@ -1,3 +1,4 @@
+# use a more recent debian image so it can run the ziti binary built with the Docker host's glibc
 FROM debian:bookworm-slim
 
 ARG ARTIFACTS_DIR=./build
diff --git a/quickstart/docker/all-in-one/README.md b/quickstart/docker/all-in-one/README.md
index 5f3a42732..78f430c9d 100644
--- a/quickstart/docker/all-in-one/README.md
+++ b/quickstart/docker/all-in-one/README.md
@@ -16,18 +16,15 @@ This is the primary use case for this project: running the `ziti edge quickstart
     docker compose pull
     ```
 
-2. Run the project.
+2. Run the project and store the PKI, DB, and YAML configs in a sub-directory.
 
     ```bash
-    docker compose up
+    ZITI_HOME=./quickstart docker compose up
     ```
 
 3. Modify configuration and bounce the container.
 
-    If you set `ZITI_HOME=./persistent`, then you would modify the configs in `./persistent/` on the Docker host.
-    Otherwise, you would modify the configs in the Docker named volume that's mounted on `/persistent`. For example,
-    `docker compose exec quickstart bash` will get you a shell in the container where you can `cd /persistent` edit the
-    configs with `vi`.
+    Modify the configs in the `./quickstart/` sub-directory adjacent to the `compose.yml` file.
 
     ```bash
     docker compose up --force-recreate
@@ -104,7 +101,7 @@ Change `Dockerfile` like this, and run `ZITI_QUICK_TAG=local docker compose up -
 checked-out source tree and run the quickstart with the build.
 
 ```dockerfile
-FROM golang:1.20-bookworm AS builder
+FROM golang:1.21-bookworm AS builder
 ARG ARTIFACTS_DIR=./build
 WORKDIR /app
 COPY go.mod go.sum ./
diff --git a/quickstart/docker/all-in-one/compose.override.yml b/quickstart/docker/all-in-one/compose.override.yml
index 9766a164c..a6325b4de 100644
--- a/quickstart/docker/all-in-one/compose.override.yml
+++ b/quickstart/docker/all-in-one/compose.override.yml
@@ -40,11 +40,12 @@ services:
     networks:
       - quickstart
     volumes:
-      - ${ZITI_HOME:-persistent}:/persistent
+      - ${ZITI_HOME:-ziti_home}:/home/ziggy
     entrypoint:
       - bash
       - -euxc
       - |
+        set -o pipefail
         ATTEMPTS=10
         DELAY=3
         until !((ATTEMPTS)) || ziti $${@} &>/dev/null; do
@@ -57,4 +58,4 @@ services:
       -- edge login
       ${EXTERNAL_DNS:-quickstart}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
       -u ${ZITI_USER:-admin} -p ${ZITI_PWD:-admin}
-      --ca /persistent/pki/root-ca/certs/root-ca.cert
+      --ca /home/ziggy/quickstart/pki/root-ca/certs/root-ca.cert
diff --git a/quickstart/docker/all-in-one/compose.yml b/quickstart/docker/all-in-one/compose.yml
index 5ea648c34..bea573164 100644
--- a/quickstart/docker/all-in-one/compose.yml
+++ b/quickstart/docker/all-in-one/compose.yml
@@ -27,33 +27,33 @@ services:
         " --password ${ZITI_PWD:-admin}"
         echo "DEBUG: run command is: ziti $${@} $${ZITI_CMD}"
         exec ziti "$${@}" $${ZITI_CMD}
-    command: -- edge quickstart --home /persistent
+    command: -- edge quickstart --home /home/ziggy/quickstart
     user: ${ZIGGY_UID:-1000}
     environment:
-      HOME: /persistent
+      HOME: /home/ziggy
       PFXLOG_NO_JSON: "${PFXLOG_NO_JSON:-true}"
     volumes:
-      # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
+      # store the quickstart state in a named volume "ziti_home" or store the quickstart state on the Docker host in a
       # directory, ZITI_HOME 
-      - ${ZITI_HOME:-persistent}:/persistent
+      - ${ZITI_HOME:-ziti_home}:/home/ziggy
     ports:
       - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}:${ZITI_CTRL_EDGE_ADVERTISED_PORT:-1280}
       - ${ZITI_INTERFACE:-0.0.0.0}:${ZITI_ROUTER_PORT:-3022}:${ZITI_ROUTER_PORT:-3022}
     depends_on:
       initialize:
         condition: service_completed_successfully
-  # this service is used to initialize the persistent volume by setting the owner to the UID of the user running the
+  # this service is used to initialize the ziti_home volume by setting the owner to the UID of the user running the
   # quickstart container
   initialize:
     image: busybox
-    command: chown -Rc ${ZIGGY_UID:-1000} /persistent
+    command: chown -Rc ${ZIGGY_UID:-1000} /home/ziggy
     user: root
     environment:
-      HOME: /persistent
+      HOME: /home/ziggy
     volumes:
-      # store the quickstart state in a named volume "persistent" or store the quickstart state on the Docker host in a
+      # store the quickstart state in a named volume "ziti_home" or store the quickstart state on the Docker host in a
       # directory, ZITI_HOME 
-      - ${ZITI_HOME:-persistent}:/persistent
+      - ${ZITI_HOME:-ziti_home}:/home/ziggy
 
 # define a custom network so that we can also define a DNS alias for the quickstart container
 networks:
@@ -62,5 +62,5 @@ networks:
 
 volumes:
   # this will not be used if you switch from named volume to bind mount volume
-  persistent:
+  ziti_home:
     driver: local
\ No newline at end of file
diff --git a/quickstart/test/compose-test.zsh b/quickstart/test/compose-test.zsh
index a597abac1..fdb98043d 100755
--- a/quickstart/test/compose-test.zsh
+++ b/quickstart/test/compose-test.zsh
@@ -91,8 +91,14 @@ fi
 # rename the simplified Compose file to the default Compose project file name
 mv ./simplified-docker-compose.yml ./compose.yml
 
-# learn the expected Go version from the Go mod file so we can pull the correct container image
-: ${ZITI_GO_VERSION:="$(awk '/^go[[:space:]]+/ {print $2}' ./go.mod)"}
+if [[ -n "${ZITI_GO_VERSION:-}" ]]; then
+    echo "INFO: Using Go version from environment variable ZITI_GO_VERSION=${ZITI_GO_VERSION}"
+else
+    # learn the expected Go version from the Go mod file so we can pull the correct container image
+    ZITI_GO_VERSION="$(awk '/^go[[:space:]]+/ {print $2}' ./go.mod)"
+    echo "INFO: Using Go version from go.mod: ${ZITI_GO_VERSION}"
+fi
+
 # make this var available in the Compose project
 sed -E \
     -e  "s/^(#[[:space:]]*)?(ZITI_PWD)=.*/\2=${ZITI_PWD}/" \

From e2370dd1eeb9f872522edec48626f630aef6dec9 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Thu, 15 Feb 2024 16:21:59 -0500
Subject: [PATCH 19/46] add proxy mode; resolves #987

---
 ziti/cmd/create/create_config_router_edge.go | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/ziti/cmd/create/create_config_router_edge.go b/ziti/cmd/create/create_config_router_edge.go
index 02e96b40d..06ad8a4d5 100644
--- a/ziti/cmd/create/create_config_router_edge.go
+++ b/ziti/cmd/create/create_config_router_edge.go
@@ -38,11 +38,12 @@ const (
 	defaultPrivate          = false
 	privateDescription      = "Create a private router config"
 	tproxyTunMode           = "tproxy"
+	proxyTunMode            = "proxy"
 	hostTunMode             = "host"
 	noneTunMode             = "none"
 	optionTunnelerMode      = "tunnelerMode"
 	defaultTunnelerMode     = hostTunMode
-	tunnelerModeDescription = "Specify tunneler mode \"" + noneTunMode + "\", \"" + hostTunMode + "\", or \"" + tproxyTunMode + "\""
+	tunnelerModeDescription = "Specify tunneler mode \"" + noneTunMode + "\", \"" + hostTunMode + "\", \"" + tproxyTunMode + "\", or \"" + proxyTunMode + "\""
 	optionLanInterface      = "lanInterface"
 	defaultLanInterface     = ""
 	lanInterfaceDescription = "The interface on host of the router to insert iptables ingress filter rules"
@@ -110,8 +111,11 @@ func (options *CreateConfigRouterOptions) runEdgeRouter(data *ConfigTemplateValu
 	}
 
 	// Make sure the tunneler mode is valid
-	if options.TunnelerMode != hostTunMode && options.TunnelerMode != tproxyTunMode && options.TunnelerMode != noneTunMode {
-		return errors.New("Unknown tunneler mode [" + options.TunnelerMode + "] provided, should be \"" + noneTunMode + "\", \"" + hostTunMode + "\", or \"" + tproxyTunMode + "\"")
+	if options.TunnelerMode != hostTunMode &&
+		options.TunnelerMode != tproxyTunMode &&
+		options.TunnelerMode != proxyTunMode &&
+		options.TunnelerMode != noneTunMode {
+		return errors.New("Unknown tunneler mode [" + options.TunnelerMode + "] provided, should be \"" + noneTunMode + "\", \"" + hostTunMode + "\", \"" + proxyTunMode + "\", or \"" + tproxyTunMode + "\"")
 	}
 
 	tmpl, err := template.New("edge-router-config").Parse(routerConfigEdgeTemplate)

From d42d38e20749756887ccd572ace3faff446ef96b Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Fri, 16 Feb 2024 09:27:48 -0500
Subject: [PATCH 20/46] fix testing generated config

---
 ziti/cmd/create/create_config_router_edge_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ziti/cmd/create/create_config_router_edge_test.go b/ziti/cmd/create/create_config_router_edge_test.go
index 7a432d163..ae642101c 100644
--- a/ziti/cmd/create/create_config_router_edge_test.go
+++ b/ziti/cmd/create/create_config_router_edge_test.go
@@ -116,7 +116,7 @@ func TestTunnelerTproxyMode(t *testing.T) {
 func TestTunnelerInvalidMode(t *testing.T) {
 	invalidMode := "invalidMode"
 
-	expectedErrorMsg := "Unknown tunneler mode [" + invalidMode + "] provided, should be \"" + noneTunMode + "\", \"" + hostTunMode + "\", or \"" + tproxyTunMode + "\""
+	expectedErrorMsg := "Unknown tunneler mode [" + invalidMode + "] provided, should be \"" + noneTunMode + "\", \"" + hostTunMode + "\", \"" + proxyTunMode + "\", or \"" + tproxyTunMode + "\""
 
 	// Create the options with both flags set to true
 	routerOptions := clearEnvAndInitializeTestData()

From 8ed418e10304e71e673fdc6134f5511755f02673 Mon Sep 17 00:00:00 2001
From: Mario Trangoni <mjtrangoni@gmail.com>
Date: Tue, 13 Feb 2024 09:07:01 +0100
Subject: [PATCH 21/46] ci: Add dependabot configuration for github-actions

Signed-off-by: Mario Trangoni <mjtrangoni@gmail.com>
---
 .github/dependabot.yml | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d921d0ffd..e62b5d7b2 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,7 +1,12 @@
 version: 2
 updates:
-- package-ecosystem: gomod
-  directory: "/"
-  schedule:
-    interval: daily
-  open-pull-requests-limit: 10
+  - package-ecosystem: gomod
+    directory: "/"
+    schedule:
+      interval: daily
+    open-pull-requests-limit: 10
+  - package-ecosystem: github-actions
+    directory: "/"
+    schedule:
+      interval: weekly
+    open-pull-requests-limit: 10

From 22965cf00a82f64aa4fa67749e1f71676dc22d6d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 20 Feb 2024 13:33:24 +0000
Subject: [PATCH 22/46] Bump actions/checkout from 3 to 4

Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
---
 .github/workflows/codeql.yml                 |  2 +-
 .github/workflows/fablab-db-creation.yml     |  2 +-
 .github/workflows/golangci-lint.yml          |  2 +-
 .github/workflows/main.yml                   | 14 +++++++-------
 .github/workflows/publish-docker-images.yml  |  2 +-
 .github/workflows/publish-linux-packages.yml |  2 +-
 .github/workflows/release-quickstart.yml     |  2 +-
 .github/workflows/test-cloudfront-proxy.yml  |  2 +-
 .github/workflows/test-quickstart.yml        |  4 ++--
 .github/workflows/update-dependency.yml      |  2 +-
 10 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 88f9ed83e..8c29cfba2 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -42,7 +42,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
diff --git a/.github/workflows/fablab-db-creation.yml b/.github/workflows/fablab-db-creation.yml
index 8f8ffd4f0..0b32738a6 100644
--- a/.github/workflows/fablab-db-creation.yml
+++ b/.github/workflows/fablab-db-creation.yml
@@ -22,7 +22,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout ziti
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           path: ziti
 
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 8f22127db..db9f57867 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -21,7 +21,7 @@ jobs:
         with:
           go-version: '1.21.x'
 
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v3
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 90cf0eac9..f7b6d47d6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -39,7 +39,7 @@ jobs:
     runs-on: macos-11
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -73,7 +73,7 @@ jobs:
     runs-on: windows-2019
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -104,7 +104,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -140,7 +140,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -171,7 +171,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -294,7 +294,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -409,7 +409,7 @@ jobs:
       ZITI_VERSION: ${{ steps.get_version.outputs.ZITI_VERSION }}
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/publish-docker-images.yml b/.github/workflows/publish-docker-images.yml
index e455e297d..d2d29ac27 100644
--- a/.github/workflows/publish-docker-images.yml
+++ b/.github/workflows/publish-docker-images.yml
@@ -19,7 +19,7 @@ jobs:
       ZITI_TUNNEL_IMAGE:     ${{ vars.ZITI_TUNNEL_IMAGE || 'docker.io/openziti/ziti-tunnel' }}
     steps:
       - name: Checkout Workspace
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Download Linux Release Artifacts
         uses: actions/download-artifact@v4
diff --git a/.github/workflows/publish-linux-packages.yml b/.github/workflows/publish-linux-packages.yml
index f1392040b..548774307 100644
--- a/.github/workflows/publish-linux-packages.yml
+++ b/.github/workflows/publish-linux-packages.yml
@@ -46,7 +46,7 @@ jobs:
       ZITI_RPM_PROD_REPO: ${{ vars.ZITI_RPM_PROD_REPO || 'zitipax-openziti-rpm-stable' }}
     steps:
       - name: Checkout Workspace
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Download Linux Release Artifacts
         uses: actions/download-artifact@v4
diff --git a/.github/workflows/release-quickstart.yml b/.github/workflows/release-quickstart.yml
index 0127871df..de896aea8 100644
--- a/.github/workflows/release-quickstart.yml
+++ b/.github/workflows/release-quickstart.yml
@@ -44,7 +44,7 @@ jobs:
           running-workflow-name: release-quickstart
 
       - name: Checkout Workspace
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Install Ziti CI
         uses: openziti/ziti-ci@v1
diff --git a/.github/workflows/test-cloudfront-proxy.yml b/.github/workflows/test-cloudfront-proxy.yml
index fda9a3826..833babab5 100644
--- a/.github/workflows/test-cloudfront-proxy.yml
+++ b/.github/workflows/test-cloudfront-proxy.yml
@@ -21,7 +21,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Shallow checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Configure Python
         shell: bash
diff --git a/.github/workflows/test-quickstart.yml b/.github/workflows/test-quickstart.yml
index 8624d223f..02241478f 100644
--- a/.github/workflows/test-quickstart.yml
+++ b/.github/workflows/test-quickstart.yml
@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Shallow checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Install zsh
         shell: bash
@@ -43,7 +43,7 @@ jobs:
       ZIGGY_UID: 1001  # let container EUID run-as GHA "runner" user to share cache, etc.
     steps:
       - name: Shallow checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Install Go
         id: setup-go
diff --git a/.github/workflows/update-dependency.yml b/.github/workflows/update-dependency.yml
index b0aac5926..509db9df3 100644
--- a/.github/workflows/update-dependency.yml
+++ b/.github/workflows/update-dependency.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Git Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 

From 25a8fb2ccac2ddaac002738c3251fb70059af7e6 Mon Sep 17 00:00:00 2001
From: Shawn Carey <shawn.carey@netfoundry.io>
Date: Tue, 20 Feb 2024 17:53:39 +0000
Subject: [PATCH 23/46] Optimize service add (#1727)

* don't check interface ips when getting ips from dns pool
* assign dns ip range to loopback instead of one-ip-at-a-time
* recycle unused dns ips
* replace $dst_hostname when calculating dial identity
* fall back to dummy resolver if server initialization fails.
---
 go.mod                                  |   1 +
 go.sum                                  |  18 +---
 router/xgress_edge_tunnel/tunneler.go   |  21 ++---
 tunnel/dns/dummy.go                     |  39 +++++++++
 tunnel/dns/file.go                      |   4 +-
 tunnel/dns/refcount.go                  |   6 +-
 tunnel/dns/resolver.go                  |   3 +-
 tunnel/dns/server.go                    |  23 ++++-
 tunnel/entities/service.go              |   4 +-
 tunnel/intercept/hosting.go             |   2 +-
 tunnel/intercept/interceptor.go         |  28 +++---
 tunnel/intercept/iputils.go             | 108 ++++++++++++++++--------
 tunnel/intercept/svcpoll.go             |   7 ++
 tunnel/intercept/tproxy/tproxy_linux.go |  44 +++++-----
 tunnel/utils/ipcalc.go                  |  90 ++++----------------
 zititest/go.mod                         |   1 +
 zititest/go.sum                         |   2 +
 17 files changed, 224 insertions(+), 177 deletions(-)
 create mode 100644 tunnel/dns/dummy.go

diff --git a/go.mod b/go.mod
index 5e8c5ae11..bfd88baa9 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
 	github.com/emirpasic/gods v1.18.1
 	github.com/fatih/color v1.16.0
 	github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
+	github.com/gaissmai/extnetip v0.4.0
 	github.com/go-acme/lego/v4 v4.15.0
 	github.com/go-openapi/errors v0.21.0
 	github.com/go-openapi/loads v0.21.5
diff --git a/go.sum b/go.sum
index 2432d74b9..3e5fb0829 100644
--- a/go.sum
+++ b/go.sum
@@ -192,11 +192,11 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/gaissmai/extnetip v0.4.0 h1:9pNd/Z6QSlkda35bug/IYuPYaPMTYRuqcxPce5Z9TTQ=
+github.com/gaissmai/extnetip v0.4.0/go.mod h1:M3NWlyFKaVosQXWXKKeIPK+5VM4U85DahdIqNYX4TK4=
 github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
-github.com/go-acme/lego/v4 v4.14.2 h1:/D/jqRgLi8Cbk33sLGtu2pX2jEg3bGJWHyV8kFuUHGM=
-github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c=
 github.com/go-acme/lego/v4 v4.15.0 h1:A7MHEU3b+TDFqhC/HmzMJnzPbyeaYvMZQBbqgvbThhU=
 github.com/go-acme/lego/v4 v4.15.0/go.mod h1:eeGhjW4zWT7Ccqa3sY7ayEqFLCAICx+mXgkMHKIkLxg=
 github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
@@ -236,8 +236,6 @@ github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgA
 github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
 github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
 github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
-github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo=
-github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM=
 github.com/go-openapi/validate v0.23.0 h1:2l7PJLzCis4YUGEoW6eoQw3WhyM65WSIcjX6SQnlfDw=
 github.com/go-openapi/validate v0.23.0/go.mod h1:EeiAZ5bmpSIOJV1WLfyYF9qp/B1ZgSaEpHTJHtN5cbE=
 github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8=
@@ -404,8 +402,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/influxdata/influxdb-client-go/v2 v2.2.2/go.mod h1:fa/d1lAdUHxuc1jedx30ZfNG573oQTQmUni3N6pcW+0=
 github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
-github.com/jedib0t/go-pretty/v6 v6.5.3 h1:GIXn6Er/anHTkVUoufs7ptEvxdD6KIhR7Axa2wYCPF0=
-github.com/jedib0t/go-pretty/v6 v6.5.3/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg=
 github.com/jedib0t/go-pretty/v6 v6.5.4 h1:gOGo0613MoqUcf0xCj+h/V3sHDaZasfv152G6/5l91s=
 github.com/jedib0t/go-pretty/v6 v6.5.4/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg=
 github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
@@ -518,8 +514,6 @@ github.com/michaelquigley/pfxlog v0.6.10 h1:IbC/H3MmSDcPlQHF1UZPQU13Dkrs0+ycWRyQ
 github.com/michaelquigley/pfxlog v0.6.10/go.mod h1:gEiNTfKEX6cJHSwRpOuqBpc8oYrlhMiDK/xMk/gV7D0=
 github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
-github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
 github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
 github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
 github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
@@ -753,9 +747,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
+github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
 github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -872,8 +866,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -975,8 +967,6 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1182,8 +1172,6 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
-golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
 golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
 golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/router/xgress_edge_tunnel/tunneler.go b/router/xgress_edge_tunnel/tunneler.go
index cf1161141..879e26da1 100644
--- a/router/xgress_edge_tunnel/tunneler.go
+++ b/router/xgress_edge_tunnel/tunneler.go
@@ -70,6 +70,17 @@ func (self *tunneler) Start(notifyClose <-chan struct{}) error {
 	log := pfxlog.Logger()
 	log.WithField("mode", self.listenOptions.mode).Info("creating interceptor")
 
+	resolver, err := dns.NewResolver(self.listenOptions.resolver)
+	if err != nil {
+		pfxlog.Logger().WithError(err).Error("failed to start DNS resolver. using dummy resolver")
+		resolver = dns.NewDummyResolver()
+	}
+
+	if err = intercept.SetDnsInterceptIpRange(self.listenOptions.dnsSvcIpRange); err != nil {
+		pfxlog.Logger().Errorf("invalid dns service IP range %s: %v", self.listenOptions.dnsSvcIpRange, err)
+		return err
+	}
+
 	if strings.HasPrefix(self.listenOptions.mode, "tproxy") {
 		tproxyConfig := tproxy.Config{
 			LanIf:            self.listenOptions.lanIf,
@@ -96,16 +107,6 @@ func (self *tunneler) Start(notifyClose <-chan struct{}) error {
 		return errors.Errorf("unsupported tunnel mode '%v'", self.listenOptions.mode)
 	}
 
-	resolver, err := dns.NewResolver(self.listenOptions.resolver)
-	if err != nil {
-		pfxlog.Logger().WithError(err).Error("failed to start DNS resolver")
-	}
-
-	if err = intercept.SetDnsInterceptIpRange(self.listenOptions.dnsSvcIpRange); err != nil {
-		pfxlog.Logger().Errorf("invalid dns service IP range %s: %v", self.listenOptions.dnsSvcIpRange, err)
-		return err
-	}
-
 	self.servicePoller.serviceListener = intercept.NewServiceListener(self.interceptor, resolver)
 	self.servicePoller.serviceListener.HandleProviderReady(self.fabricProvider)
 
diff --git a/tunnel/dns/dummy.go b/tunnel/dns/dummy.go
new file mode 100644
index 000000000..e109ab1ae
--- /dev/null
+++ b/tunnel/dns/dummy.go
@@ -0,0 +1,39 @@
+package dns
+
+import (
+	"github.com/michaelquigley/pfxlog"
+	"net"
+)
+
+type dummy struct{}
+
+func (d dummy) AddHostname(_ string, _ net.IP) error {
+	pfxlog.Logger().Warnf("dummy resolver does not store hostname/ip mappings")
+	return nil
+}
+
+func (d dummy) AddDomain(_ string, _ func(string) (net.IP, error)) error {
+	pfxlog.Logger().Warnf("dummy resolver does not store hostname/ip mappings")
+	return nil
+}
+
+func (d dummy) Lookup(_ net.IP) (string, error) {
+	pfxlog.Logger().Warnf("dummy resolver does not store hostname/ip mappings")
+	return "", nil
+}
+
+func (d dummy) RemoveHostname(_ string) net.IP {
+	return nil
+}
+
+func (d dummy) RemoveDomain(_ string) {
+}
+
+func (d dummy) Cleanup() error {
+	return nil
+}
+
+func NewDummyResolver() Resolver {
+	pfxlog.Logger().Warnf("dummy resolver does not store hostname/ip mappings")
+	return &dummy{}
+}
diff --git a/tunnel/dns/file.go b/tunnel/dns/file.go
index 04584699e..6e09cc214 100644
--- a/tunnel/dns/file.go
+++ b/tunnel/dns/file.go
@@ -54,6 +54,8 @@ func (h *hostFile) AddDomain(name string, _ func(string) (net.IP, error)) error
 	return fmt.Errorf("cannot add wildcard domain[%s] to hostfile resolver", name)
 }
 
+func (h *hostFile) RemoveDomain(string) {}
+
 func (h *hostFile) Lookup(_ net.IP) (string, error) {
 	return "", fmt.Errorf("not implemented")
 }
@@ -81,7 +83,7 @@ func (h *hostFile) AddHostname(hostname string, ip net.IP) error {
 	return nil
 }
 
-func (h *hostFile) RemoveHostname(_ string) error {
+func (h *hostFile) RemoveHostname(_ string) net.IP {
 	return nil
 }
 
diff --git a/tunnel/dns/refcount.go b/tunnel/dns/refcount.go
index 7a0cdc1d7..55b67b4b9 100644
--- a/tunnel/dns/refcount.go
+++ b/tunnel/dns/refcount.go
@@ -25,6 +25,10 @@ func (self *RefCountingResolver) AddDomain(name string, cb func(string) (net.IP,
 	return self.wrapped.AddDomain(name, cb)
 }
 
+func (self *RefCountingResolver) RemoveDomain(name string) {
+	self.wrapped.RemoveDomain(name)
+}
+
 func (self *RefCountingResolver) AddHostname(s string, ip net.IP) error {
 	err := self.wrapped.AddHostname(s, ip)
 	if err != nil {
@@ -38,7 +42,7 @@ func (self *RefCountingResolver) AddHostname(s string, ip net.IP) error {
 	return err
 }
 
-func (self *RefCountingResolver) RemoveHostname(s string) error {
+func (self *RefCountingResolver) RemoveHostname(s string) net.IP {
 	val := self.names.Upsert(s, 1, func(exist bool, valueInMap int, newValue int) int {
 		if exist {
 			return valueInMap - 1
diff --git a/tunnel/dns/resolver.go b/tunnel/dns/resolver.go
index 2c1addcd1..d8279b632 100644
--- a/tunnel/dns/resolver.go
+++ b/tunnel/dns/resolver.go
@@ -22,7 +22,8 @@ type Resolver interface {
 	AddHostname(string, net.IP) error
 	AddDomain(string, func(string) (net.IP, error)) error
 	Lookup(net.IP) (string, error)
-	RemoveHostname(string) error
+	RemoveHostname(string) net.IP
+	RemoveDomain(string)
 	Cleanup() error
 }
 
diff --git a/tunnel/dns/server.go b/tunnel/dns/server.go
index 303d8df87..c6652a77f 100644
--- a/tunnel/dns/server.go
+++ b/tunnel/dns/server.go
@@ -186,12 +186,13 @@ func (r *resolver) getAddress(name string) (net.IP, error) {
 		de, ok := r.domains[canonical]
 
 		if ok {
+			name = name[:len(name)-1]
 			ip, err := de.getIP(name)
 			if err != nil {
 				return nil, err
 			}
 			log.Debugf("assigned %v => %v", name, ip)
-			_ = r.AddHostname(name[:len(name)-1], ip) // this resolver impl never returns an error
+			_ = r.AddHostname(name, ip) // this resolver impl never returns an error
 			return ip, err
 		}
 	}
@@ -250,6 +251,18 @@ func (r *resolver) AddDomain(name string, ipCB func(string) (net.IP, error)) err
 	return nil
 }
 
+func (r *resolver) RemoveDomain(name string) {
+	if name[0] != '*' {
+		log.Warnf("invalid wildcard domain '%s'", name)
+		return
+	}
+	domainSfx := name[1:] + "."
+	r.domainsMtx.Lock()
+	defer r.domainsMtx.Unlock()
+	log.Infof("removing domain %s from resolver", domainSfx)
+	delete(r.domains, domainSfx)
+}
+
 func (r *resolver) AddHostname(hostname string, ip net.IP) error {
 	r.namesMtx.Lock()
 	defer r.namesMtx.Unlock()
@@ -277,18 +290,20 @@ func (r *resolver) Lookup(ip net.IP) (string, error) {
 	return "", errors.New("not found")
 }
 
-func (r *resolver) RemoveHostname(hostname string) error {
+func (r *resolver) RemoveHostname(hostname string) net.IP {
 	r.namesMtx.Lock()
 	defer r.namesMtx.Unlock()
 
 	key := strings.ToLower(hostname) + "."
-	if ip, ok := r.names[key]; ok {
+	var ip net.IP
+	var ok bool
+	if ip, ok = r.names[key]; ok {
 		log.Infof("removing %s from resolver", hostname)
 		delete(r.ips, ip.String())
 		delete(r.names, key)
 	}
 
-	return nil
+	return ip
 }
 
 func (r *resolver) Cleanup() error {
diff --git a/tunnel/entities/service.go b/tunnel/entities/service.go
index a7a77e918..fa2a5a7a1 100644
--- a/tunnel/entities/service.go
+++ b/tunnel/entities/service.go
@@ -164,7 +164,7 @@ func makeAllowedAddress(addr string) (allowedAddress, error) {
 		return &domainAddress{domain: strings.ToLower(addr)}, nil
 	}
 
-	if _, cidr, err := utils.GetDialIP(addr); err == nil {
+	if cidr, err := utils.GetCidr(addr); err == nil {
 		return &cidrAddress{cidr: *cidr}, nil
 	}
 
@@ -291,7 +291,7 @@ func (self *HostV1Config) GetAllowedSourceAddressRoutes() ([]*net.IPNet, error)
 	var routes []*net.IPNet
 	for _, addr := range self.AllowedSourceAddresses {
 		// need to get CIDR from address - iputils.getInterceptIp?
-		_, ipNet, err := utils.GetDialIP(addr)
+		ipNet, err := utils.GetCidr(addr)
 		if err != nil {
 			return nil, errors.Errorf("failed to parse allowed source address '%s': %v", addr, err)
 		}
diff --git a/tunnel/intercept/hosting.go b/tunnel/intercept/hosting.go
index 79da2b60e..5d403f68b 100644
--- a/tunnel/intercept/hosting.go
+++ b/tunnel/intercept/hosting.go
@@ -193,7 +193,7 @@ func (self *hostingContext) SetCloseCallback(f func()) {
 func (self *hostingContext) OnClose() {
 	log := pfxlog.Logger().WithField("service", self.service.Name)
 	for _, addr := range self.config.AllowedSourceAddresses {
-		_, ipNet, err := utils.GetDialIP(addr)
+		ipNet, err := utils.GetCidr(addr)
 		if err != nil {
 			log.WithError(err).Error("failed to get dial IP")
 		} else if self.addrTracker.RemoveAddress(ipNet.String()) {
diff --git a/tunnel/intercept/interceptor.go b/tunnel/intercept/interceptor.go
index 1088ea1da..b7b5b8fde 100644
--- a/tunnel/intercept/interceptor.go
+++ b/tunnel/intercept/interceptor.go
@@ -43,12 +43,13 @@ type Interceptor interface {
 // - service name - when a service is removed (e.g. from an appwan)
 
 type InterceptAddress struct {
-	cidr       *net.IPNet
-	lowPort    uint16
-	highPort   uint16
-	protocol   string
-	TproxySpec []string
-	AcceptSpec []string
+	cidr          *net.IPNet
+	routeRequired bool
+	lowPort       uint16
+	highPort      uint16
+	protocol      string
+	TproxySpec    []string
+	AcceptSpec    []string
 }
 
 func (addr *InterceptAddress) Proto() string {
@@ -59,6 +60,10 @@ func (addr *InterceptAddress) IpNet() *net.IPNet {
 	return addr.cidr
 }
 
+func (addr *InterceptAddress) RouteRequired() bool {
+	return addr.routeRequired
+}
+
 func (addr *InterceptAddress) LowPort() uint16 {
 	return addr.lowPort
 }
@@ -82,14 +87,15 @@ type InterceptAddrCB interface {
 
 func GetInterceptAddresses(service *entities.Service, protocols []string, resolver dns.Resolver, addressCB InterceptAddrCB) error {
 	for _, addr := range service.InterceptV1Config.Addresses {
-		err := getInterceptIP(service, addr, resolver, func(ip net.IP, ipNet *net.IPNet) {
+		err := getInterceptIP(service, addr, resolver, func(ipNet *net.IPNet, routeRequired bool) {
 			for _, protocol := range protocols {
 				for _, portRange := range service.InterceptV1Config.PortRanges {
 					addr := &InterceptAddress{
-						cidr:     ipNet,
-						lowPort:  portRange.Low,
-						highPort: portRange.High,
-						protocol: protocol}
+						cidr:          ipNet,
+						routeRequired: routeRequired,
+						lowPort:       portRange.Low,
+						highPort:      portRange.High,
+						protocol:      protocol}
 					addressCB.Apply(addr)
 				}
 			}
diff --git a/tunnel/intercept/iputils.go b/tunnel/intercept/iputils.go
index d0e2f4d77..74888475c 100644
--- a/tunnel/intercept/iputils.go
+++ b/tunnel/intercept/iputils.go
@@ -17,85 +17,121 @@
 package intercept
 
 import (
+	"container/list"
 	"fmt"
+	"github.com/gaissmai/extnetip"
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/ziti/tunnel/dns"
 	"github.com/openziti/ziti/tunnel/entities"
 	"github.com/openziti/ziti/tunnel/utils"
 	"net"
+	"net/netip"
+	"sync"
 )
 
-var dnsIpLow, dnsIpHigh net.IP
+var dnsPrefix netip.Prefix
+var dnsCurrentIp netip.Addr
+var dnsCurrentIpMtx sync.Mutex
+var dnsRecycledIps *list.List
 
 func SetDnsInterceptIpRange(cidr string) error {
-	ip, ipnet, err := net.ParseCIDR(cidr)
+	prefix, err := netip.ParsePrefix(cidr)
 	if err != nil {
 		return fmt.Errorf("invalid cidr %s: %v", cidr, err)
 	}
 
-	var ips []net.IP
-	for ip = ip.Mask(ipnet.Mask); ipnet.Contains(ip); utils.IncIP(ip) {
-		a := make(net.IP, len(ip))
-		copy(a, ip)
-		ips = append(ips, a)
-	}
+	dnsPrefix = prefix
+	// get last ip in range for logging
+	_, dnsIpHigh := extnetip.Range(dnsPrefix)
 
-	// remove network address and broadcast address
-	dnsIpLow = ips[1]
-	dnsIpHigh = ips[len(ips)-2]
+	dnsCurrentIpMtx.Lock()
+	dnsCurrentIp = dnsPrefix.Addr()
+	dnsRecycledIps = list.New()
+	dnsCurrentIpMtx.Unlock()
+	pfxlog.Logger().Infof("dns intercept IP range: %v - %v", dnsCurrentIp, dnsIpHigh)
+	return nil
+}
 
-	if len(dnsIpLow) != len(dnsIpHigh) {
-		return fmt.Errorf("lower dns IP length %d differs from upper dns IP length %d", len(dnsIpLow), len(dnsIpHigh))
+func GetDnsInterceptIpRange() *net.IPNet {
+	if !dnsPrefix.IsValid() {
+		if err := SetDnsInterceptIpRange("100.64.0.1/10"); err != nil {
+			pfxlog.Logger().WithError(err).Errorf("Failed to set DNS intercept range")
+		}
+	}
+	return &net.IPNet{
+		IP:   dnsPrefix.Addr().AsSlice(),
+		Mask: net.CIDRMask(dnsPrefix.Bits(), dnsPrefix.Addr().BitLen()),
 	}
-
-	pfxlog.Logger().Infof("dns intercept IP range: %s - %s", dnsIpLow.String(), dnsIpHigh.String())
-	return nil
 }
 
 func cleanUpFunc(hostname string, resolver dns.Resolver) func() {
 	f := func() {
-		if err := resolver.RemoveHostname(hostname); err != nil {
-			pfxlog.Logger().WithError(err).Errorf("failed to remove host mapping from resolver: %v ", hostname)
+		ip := resolver.RemoveHostname(hostname)
+		if ip != nil {
+			dnsCurrentIpMtx.Lock()
+			defer dnsCurrentIpMtx.Unlock()
+			addr, _ := netip.AddrFromSlice(ip)
+			dnsRecycledIps.PushBack(addr)
 		}
 	}
 	return f
 }
 
-func getInterceptIP(svc *entities.Service, hostname string, resolver dns.Resolver, addrCB func(net.IP, *net.IPNet)) error {
+func getDnsIp(host string, addrCB func(*net.IPNet, bool), svc *entities.Service, resolver dns.Resolver) (net.IP, error) {
+	dnsCurrentIpMtx.Lock()
+	defer dnsCurrentIpMtx.Unlock()
+	var ip netip.Addr
+
+	// look for returned IPs first
+	if dnsRecycledIps.Len() > 0 {
+		e := dnsRecycledIps.Front()
+		ip = e.Value.(netip.Addr)
+		dnsRecycledIps.Remove(e)
+		pfxlog.Logger().Debugf("using recycled ip %v for hostname %s", ip, host)
+	} else {
+		ip = dnsCurrentIp.Next()
+		if ip.IsValid() && dnsPrefix.Contains(ip) {
+			dnsCurrentIp = ip
+		} else {
+			return nil, fmt.Errorf("cannot allocate ip address: ip range exhausted")
+		}
+	}
+
+	addr := &net.IPNet{IP: ip.AsSlice(), Mask: net.CIDRMask(ip.BitLen(), ip.BitLen())}
+	addrCB(addr, false) // no route is needed because the dns cidr was added to "lo" at startup
+	svc.AddCleanupAction(cleanUpFunc(host, resolver))
+	return ip.AsSlice(), nil
+}
+
+func getInterceptIP(svc *entities.Service, hostname string, resolver dns.Resolver, addrCB func(*net.IPNet, bool)) error {
 	logger := pfxlog.Logger()
 
+	// handle wildcard domain - IPs will be allocated when matching hostnames are queried
 	if hostname[0] == '*' {
 		err := resolver.AddDomain(hostname, func(host string) (net.IP, error) {
-			var ip net.IP
-			var err error
-			ip, err = utils.NextIP(dnsIpLow, dnsIpHigh)
-
-			if err == nil {
-				addrCB(ip, utils.Ip2IPnet(ip))
-				svc.AddCleanupAction(cleanUpFunc(host, resolver))
-			}
-			return ip, err
+			return getDnsIp(host, addrCB, svc, resolver)
 		})
+		if err == nil {
+			svc.AddCleanupAction(func() { resolver.RemoveDomain(hostname) })
+		}
 		return err
 	}
 
-	ip, ipNet, err := utils.GetDialIP(hostname)
+	// handle IP or CIDR
+	ipNet, err := utils.GetCidr(hostname)
 	if err == nil {
-		addrCB(ip, ipNet)
+		addrCB(ipNet, true)
 		return err
 	}
 
-	ip, _ = utils.NextIP(dnsIpLow, dnsIpHigh)
-	if ip == nil {
+	// handle hostnames
+	ip, err := getDnsIp(hostname, addrCB, svc, resolver)
+	if err != nil {
 		return fmt.Errorf("invalid IP address or unresolvable hostname: %s", hostname)
 	}
 	if err = resolver.AddHostname(hostname, ip); err != nil {
 		logger.WithError(err).Errorf("failed to add host/ip mapping to resolver: %v -> %v", hostname, ip)
 	}
 
-	svc.AddCleanupAction(cleanUpFunc(hostname, resolver))
-
-	ipNet = utils.Ip2IPnet(ip)
-	addrCB(ip, ipNet)
 	return nil
 }
diff --git a/tunnel/intercept/svcpoll.go b/tunnel/intercept/svcpoll.go
index 256fecfa8..0136ab1a7 100644
--- a/tunnel/intercept/svcpoll.go
+++ b/tunnel/intercept/svcpoll.go
@@ -43,6 +43,7 @@ var sourceIpVar = "$" + tunnel.SourceIpKey
 var sourcePortVar = "$" + tunnel.SourcePortKey
 var dstIpVar = "$" + tunnel.DestinationIpKey
 var destPortVar = "$" + tunnel.DestinationPortKey
+var destHostnameVar = "$" + tunnel.DestinationHostname
 
 func NewServiceListenerGroup(interceptor Interceptor, resolver dns.Resolver) *ServiceListenerGroup {
 	return &ServiceListenerGroup{
@@ -370,6 +371,12 @@ func (self *ServiceListener) getTemplatingProvider(template string) (entities.Te
 		result = strings.ReplaceAll(result, sourcePortVar, sourceAddrPort)
 		result = strings.ReplaceAll(result, dstIpVar, destAddrIp)
 		result = strings.ReplaceAll(result, destPortVar, destAddrPort)
+		if self.resolver != nil {
+			destHostname, err := self.resolver.Lookup(net.ParseIP(destAddrIp))
+			if err == nil {
+				result = strings.ReplaceAll(result, destHostnameVar, destHostname)
+			}
+		}
 		return result
 	}, nil
 }
diff --git a/tunnel/intercept/tproxy/tproxy_linux.go b/tunnel/intercept/tproxy/tproxy_linux.go
index 6a8149acd..fa126ac57 100644
--- a/tunnel/intercept/tproxy/tproxy_linux.go
+++ b/tunnel/intercept/tproxy/tproxy_linux.go
@@ -103,6 +103,13 @@ func New(config Config) (intercept.Interceptor, error) {
 	log.Infof("tproxy config: udpIdleTimeout   =  [%s]", self.udpIdleTimeout.String())
 	log.Infof("tproxy config: udpCheckInterval =  [%s]", self.udpCheckInterval.String())
 
+	dnsNet := intercept.GetDnsInterceptIpRange()
+	err := router.AddLocalAddress(dnsNet, "lo")
+	if err != nil {
+		log.WithError(err).Errorf("unable to add %v to lo", dnsNet)
+		return nil, err
+	}
+
 	if self.diverter != "" {
 		cmd := exec.Command(self.diverter, "-V")
 		out, err := cmd.CombinedOutput()
@@ -137,7 +144,7 @@ func New(config Config) (intercept.Interceptor, error) {
 		logrus.Infof("no lan interface specified with '-lanIf'. please ensure firewall accepts intercepted service addresses")
 	}
 
-	return self, nil
+	return self, err
 }
 
 type alwaysRemoveAddressTracker struct{}
@@ -164,6 +171,11 @@ func (self *interceptor) Stop() {
 	})
 	self.serviceProxies.Clear()
 	self.cleanupChains()
+	dnsNet := intercept.GetDnsInterceptIpRange()
+	err := router.RemoveLocalAddress(dnsNet, "lo")
+	if err != nil {
+		logrus.WithError(err).Errorf("failed to remove route for dns IP range '%v' on 'lo'", dnsNet)
+	}
 }
 
 func (self *interceptor) Intercept(service *entities.Service, resolver dns.Resolver, tracker intercept.AddressTracker) error {
@@ -505,10 +517,11 @@ func (self *tProxy) intercept(service *entities.Service, resolver dns.Resolver,
 
 func (self *tProxy) addInterceptAddr(interceptAddr *intercept.InterceptAddress, service *entities.Service, port IPPortAddr, tracker intercept.AddressTracker) error {
 	ipNet := interceptAddr.IpNet()
-	if err := router.AddLocalAddress(ipNet, "lo"); err != nil {
-		return errors.Wrapf(err, "failed to add local route %v", ipNet)
+	if interceptAddr.RouteRequired() {
+		if err := router.AddLocalAddress(ipNet, "lo"); err != nil {
+			return errors.Wrapf(err, "failed to add local route %v", ipNet)
+		}
 	}
-	tracker.AddAddress(ipNet.String())
 	self.addresses = append(self.addresses, interceptAddr)
 
 	if self.interceptor.diverter != "" {
@@ -608,23 +621,12 @@ func (self *tProxy) StopIntercepting(tracker intercept.AddressTracker) error {
 		}
 
 		ipNet := addr.IpNet()
-		if tracker.RemoveAddress(ipNet.String()) {
-			err := router.RemoveLocalAddress(ipNet, "lo")
-			if err != nil {
-				errorList = append(errorList, err)
-				log.WithError(err).Errorf("failed to remove route %v for service %s", ipNet, *self.service.Name)
-			} else {
-				host, hostErr := self.resolver.Lookup(ipNet.IP)
-				if hostErr == nil {
-					hostErr = self.resolver.RemoveHostname(host)
-					if hostErr == nil {
-						log.Debugf("Removed hostname: %v from Resolver", host)
-					} else {
-						log.Debugf("Could not remove hostname: %v from Resolver", host)
-					}
-				} else {
-					log.Debugf("failed to find resolver entry for %v in service %s",
-						ipNet, *self.service.Name)
+		if addr.RouteRequired() {
+			if tracker.RemoveAddress(ipNet.String()) {
+				err := router.RemoveLocalAddress(ipNet, "lo")
+				if err != nil {
+					errorList = append(errorList, err)
+					log.WithError(err).Errorf("failed to remove route %v for service %s", ipNet, *self.service.Name)
 				}
 			}
 		}
diff --git a/tunnel/utils/ipcalc.go b/tunnel/utils/ipcalc.go
index c501ec2d5..46de704e1 100644
--- a/tunnel/utils/ipcalc.go
+++ b/tunnel/utils/ipcalc.go
@@ -17,85 +17,27 @@
 package utils
 
 import (
-	"bytes"
-	"github.com/michaelquigley/pfxlog"
-	"github.com/pkg/errors"
+	"fmt"
 	"net"
+	"net/netip"
 )
 
-func IsLocallyAssigned(addr, lower, upper net.IP) bool {
-	return bytes.Compare(addr.To16(), lower.To16()) >= 0 && bytes.Compare(addr.To16(), upper.To16()) <= 0
-}
-
-// return the next available IP address in the range of provided IPs
-func NextIP(lower, upper net.IP) (net.IP, error) {
-	usedAddrs, err := AllInterfaceAddrs()
-	if err != nil {
-		return nil, err
-	}
-
-	// need to make a copy of lower net.IP, since they're just byte arrays. Otherwise
-	// we're continually changing the lower ip globally
-	ip := net.IP(make([]byte, len(lower)))
-	copy(ip, lower)
-
-	for ; !ip.Equal(upper); IncIP(ip) {
-		inUse := false
-		for _, usedAddr := range usedAddrs {
-			usedIP, _, _ := net.ParseCIDR(usedAddr.String())
-			if ip.Equal(usedIP) {
-				inUse = true
-				break
-			}
-		}
-		if !inUse {
-			return ip, nil
-		}
-	}
-
-	return nil, nil
-}
-
-func IncIP(ip net.IP) {
-	for i := len(ip) - 1; i >= 0; i-- {
-		ip[i]++
-		if ip[i] > 0 {
-			break
-		}
-	}
-}
-
-// Return the length of a full prefix (no subnetting) for the given IP address.
-// Returns 32 for ipv4 addresses, and 128 for ipv6 addresses.
-func AddrBits(ip net.IP) int {
-	if ip == nil {
-		return 0
-	} else if ip.To4() != nil {
-		return net.IPv4len * 8
-	} else if ip.To16() != nil {
-		return net.IPv6len * 8
-	}
-
-	pfxlog.Logger().Infof("invalid IP address %s", ip.String())
-	return 0
-}
-
-func Ip2IPnet(ip net.IP) *net.IPNet {
-	prefixLen := AddrBits(ip)
-	ipNet := &net.IPNet{IP: ip, Mask: net.CIDRMask(prefixLen, prefixLen)}
-	return ipNet
-}
-
-func GetDialIP(addr string) (net.IP, *net.IPNet, error) {
-	// hostname is an ip address, return it
-	if parsedIP := net.ParseIP(addr); parsedIP != nil {
-		ipNet := Ip2IPnet(parsedIP)
-		return parsedIP, ipNet, nil
+func GetCidr(ipOrCidr string) (*net.IPNet, error) {
+	ip, err := netip.ParseAddr(ipOrCidr)
+	if err == nil {
+		return &net.IPNet{
+			IP:   ip.AsSlice(),
+			Mask: net.CIDRMask(ip.BitLen(), ip.BitLen()),
+		}, nil
 	}
 
-	if parsedIP, cidr, err := net.ParseCIDR(addr); err == nil {
-		return parsedIP, cidr, nil
+	pfx, err := netip.ParsePrefix(ipOrCidr)
+	if err == nil {
+		return &net.IPNet{
+			IP:   pfx.Addr().AsSlice(),
+			Mask: net.CIDRMask(pfx.Bits(), pfx.Addr().BitLen()),
+		}, nil
 	}
 
-	return nil, nil, errors.Errorf("could not parse '%s' as IP or CIDR", addr)
+	return nil, fmt.Errorf("failed to parse '%v' as IP or CIDR", ipOrCidr)
 }
diff --git a/zititest/go.mod b/zititest/go.mod
index f020add91..d8a172c53 100644
--- a/zititest/go.mod
+++ b/zititest/go.mod
@@ -64,6 +64,7 @@ require (
 	github.com/felixge/httpsnoop v1.0.3 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect
+	github.com/gaissmai/extnetip v0.4.0 // indirect
 	github.com/go-acme/lego/v4 v4.15.0 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
 	github.com/go-logr/logr v1.4.1 // indirect
diff --git a/zititest/go.sum b/zititest/go.sum
index 0d8686336..2cdc64793 100644
--- a/zititest/go.sum
+++ b/zititest/go.sum
@@ -197,6 +197,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/gaissmai/extnetip v0.4.0 h1:9pNd/Z6QSlkda35bug/IYuPYaPMTYRuqcxPce5Z9TTQ=
+github.com/gaissmai/extnetip v0.4.0/go.mod h1:M3NWlyFKaVosQXWXKKeIPK+5VM4U85DahdIqNYX4TK4=
 github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=

From 935c89bb7dede0b073c06a6bc7f7c07357caae22 Mon Sep 17 00:00:00 2001
From: Andrew Martinez <andrew.p.martinez@gmail.com>
Date: Thu, 22 Feb 2024 14:19:27 -0500
Subject: [PATCH 24/46] fixes 1354 adds hostname/domain to env info

---
 common/pb/cmd_pb/cmd.pb.go                    |   2 +-
 common/pb/ctrl_pb/ctrl.pb.go                  |   2 +-
 common/pb/edge_cmd_pb/edge_cmd.pb.go          | 637 ++++++++--------
 common/pb/edge_cmd_pb/edge_cmd.proto          |   2 +
 common/pb/edge_ctrl_pb/edge_ctrl.pb.go        | 697 +++++++++---------
 common/pb/edge_ctrl_pb/edge_ctrl.proto        |   2 +
 common/pb/edge_mgmt_pb/edge_mgmt.pb.go        |   2 +-
 common/pb/mgmt_pb/mgmt.pb.go                  |   2 +-
 controller/db/identity_store.go               |   8 +
 controller/handler_edge_ctrl/common_tunnel.go |   2 +
 .../internal/routes/identity_api_model.go     |   2 +
 controller/model/identity_manager.go          |   6 +
 controller/model/identity_model.go            |  12 +-
 go.mod                                        |  10 +-
 go.sum                                        |  31 +-
 tests/auth_cert_test.go                       |   8 +-
 zititest/go.mod                               |  10 +-
 zititest/go.sum                               |   5 +
 18 files changed, 752 insertions(+), 688 deletions(-)

diff --git a/common/pb/cmd_pb/cmd.pb.go b/common/pb/cmd_pb/cmd.pb.go
index 8f17c8487..fb9961620 100644
--- a/common/pb/cmd_pb/cmd.pb.go
+++ b/common/pb/cmd_pb/cmd.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v3.21.12
+// 	protoc        v4.23.4
 // source: cmd.proto
 
 package cmd_pb
diff --git a/common/pb/ctrl_pb/ctrl.pb.go b/common/pb/ctrl_pb/ctrl.pb.go
index 16e4e1a94..7b258309c 100644
--- a/common/pb/ctrl_pb/ctrl.pb.go
+++ b/common/pb/ctrl_pb/ctrl.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v3.21.12
+// 	protoc        v4.23.4
 // source: ctrl.proto
 
 package ctrl_pb
diff --git a/common/pb/edge_cmd_pb/edge_cmd.pb.go b/common/pb/edge_cmd_pb/edge_cmd.pb.go
index b00cde58d..3918f58a1 100644
--- a/common/pb/edge_cmd_pb/edge_cmd.pb.go
+++ b/common/pb/edge_cmd_pb/edge_cmd.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v3.21.12
+// 	protoc        v4.23.4
 // source: edge_cmd.proto
 
 package edge_cmd_pb
@@ -3466,6 +3466,8 @@ type Identity_EnvInfo struct {
 	Os        string `protobuf:"bytes,2,opt,name=Os,proto3" json:"Os,omitempty"`
 	OsRelease string `protobuf:"bytes,3,opt,name=OsRelease,proto3" json:"OsRelease,omitempty"`
 	OsVersion string `protobuf:"bytes,4,opt,name=OsVersion,proto3" json:"OsVersion,omitempty"`
+	Domain    string `protobuf:"bytes,5,opt,name=Domain,proto3" json:"Domain,omitempty"`
+	Hostname  string `protobuf:"bytes,6,opt,name=Hostname,proto3" json:"Hostname,omitempty"`
 }
 
 func (x *Identity_EnvInfo) Reset() {
@@ -3528,6 +3530,20 @@ func (x *Identity_EnvInfo) GetOsVersion() string {
 	return ""
 }
 
+func (x *Identity_EnvInfo) GetDomain() string {
+	if x != nil {
+		return x.Domain
+	}
+	return ""
+}
+
+func (x *Identity_EnvInfo) GetHostname() string {
+	if x != nil {
+		return x.Hostname
+	}
+	return ""
+}
+
 type Identity_SdkInfo struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -4512,7 +4528,7 @@ var file_edge_cmd_proto_rawDesc = []byte{
 	0x61, 0x69, 0x6d, 0x73, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x42, 0x09, 0x0a, 0x07,
 	0x5f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x61, 0x75, 0x64, 0x69,
 	0x65, 0x6e, 0x63, 0x65, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70,
-	0x72, 0x69, 0x6e, 0x74, 0x22, 0x95, 0x0c, 0x0a, 0x08, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
+	0x72, 0x69, 0x6e, 0x74, 0x22, 0xca, 0x0c, 0x0a, 0x08, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
 	0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
 	0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
 	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20,
@@ -4573,323 +4589,326 @@ var file_edge_cmd_proto_rawDesc = []byte{
 	0x6e, 0x74, 0x69, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
 	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
 	0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x04, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
-	0x65, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x88, 0x01, 0x01, 0x1a, 0x69, 0x0a, 0x07, 0x45, 0x6e,
-	0x76, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x41, 0x72, 0x63, 0x68, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x04, 0x41, 0x72, 0x63, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x73, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73, 0x52,
-	0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x73,
-	0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73, 0x56, 0x65, 0x72,
-	0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x73, 0x56, 0x65,
-	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xa1, 0x01, 0x0a, 0x07, 0x53, 0x64, 0x6b, 0x49, 0x6e, 0x66,
-	0x6f, 0x12, 0x14, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x05, 0x41, 0x70, 0x70, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x41, 0x70, 0x70, 0x56, 0x65,
-	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x41, 0x70, 0x70,
-	0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x42, 0x72, 0x61, 0x6e, 0x63,
-	0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x12,
-	0x1a, 0x0a, 0x08, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x08, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x54,
-	0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
-	0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67,
-	0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
-	0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
-	0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61,
-	0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4c,
-	0x0a, 0x1e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67,
-	0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
-	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
-	0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x46, 0x0a, 0x18,
-	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f,
-	0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
-	0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
-	0x3a, 0x02, 0x38, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f,
-	0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x0d, 0x0a, 0x0b,
-	0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f,
-	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x41, 0x74, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x64,
-	0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x22, 0xcd, 0x01, 0x0a,
-	0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57,
-	0x69, 0x74, 0x68, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x43, 0x6d,
-	0x64, 0x12, 0x36, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f,
-	0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52,
-	0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3e, 0x0a, 0x0b, 0x65, 0x6e, 0x72,
-	0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c,
-	0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70,
-	0x62, 0x2e, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0b, 0x65, 0x6e,
-	0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x31, 0x0a, 0x03, 0x63, 0x74, 0x78,
-	0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64,
-	0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
-	0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x22, 0x9d, 0x02, 0x0a,
-	0x03, 0x4d, 0x66, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03,
-	0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
-	0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x66, 0x61, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e,
-	0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x56,
-	0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69,
-	0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65,
-	0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69,
-	0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63,
-	0x72, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65,
-	0x74, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64,
-	0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65,
-	0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45,
-	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67,
-	0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75,
-	0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa0, 0x0a, 0x0a,
-	0x0c, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x0e, 0x0a,
-	0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a,
-	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
-	0x65, 0x12, 0x3c, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
-	0x28, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e,
-	0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e,
-	0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12,
-	0x16, 0x0a, 0x06, 0x74, 0x79, 0x70, 0x65, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x06, 0x74, 0x79, 0x70, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
-	0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
-	0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75,
-	0x74, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x41,
-	0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x03, 0x6d, 0x61, 0x63,
-	0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64,
-	0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72,
-	0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4d, 0x61, 0x63, 0x48, 0x00, 0x52, 0x03, 0x6d, 0x61,
-	0x63, 0x12, 0x36, 0x0a, 0x03, 0x6d, 0x66, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22,
-	0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70,
-	0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4d,
-	0x66, 0x61, 0x48, 0x00, 0x52, 0x03, 0x6d, 0x66, 0x61, 0x12, 0x3f, 0x0a, 0x06, 0x6f, 0x73, 0x4c,
-	0x69, 0x73, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x7a, 0x69, 0x74, 0x69,
-	0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73,
-	0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4f, 0x73, 0x4c, 0x69, 0x73, 0x74,
-	0x48, 0x00, 0x52, 0x06, 0x6f, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x42, 0x0a, 0x07, 0x70, 0x72,
-	0x6f, 0x63, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a, 0x69,
-	0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50,
-	0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x50, 0x72, 0x6f, 0x63,
-	0x65, 0x73, 0x73, 0x48, 0x00, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x51,
-	0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x18, 0x0b,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65,
-	0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43,
-	0x68, 0x65, 0x63, 0x6b, 0x2e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x75, 0x6c, 0x74,
-	0x69, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x75, 0x6c, 0x74,
-	0x69, 0x12, 0x42, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01,
-	0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
-	0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65,
-	0x63, 0x6b, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x07, 0x64, 0x6f,
-	0x6d, 0x61, 0x69, 0x6e, 0x73, 0x1a, 0x29, 0x0a, 0x03, 0x4d, 0x61, 0x63, 0x12, 0x22, 0x0a, 0x0c,
-	0x6d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
-	0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73,
-	0x1a, 0xaf, 0x01, 0x0a, 0x03, 0x4d, 0x66, 0x61, 0x12, 0x26, 0x0a, 0x0e, 0x54, 0x69, 0x6d, 0x65,
-	0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
-	0x52, 0x0e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73,
-	0x12, 0x22, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4f, 0x6e, 0x57, 0x61, 0x6b, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4f, 0x6e,
-	0x57, 0x61, 0x6b, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4f, 0x6e,
-	0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x50, 0x72,
-	0x6f, 0x6d, 0x70, 0x74, 0x4f, 0x6e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x34, 0x0a, 0x15,
-	0x49, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x45, 0x6e, 0x64, 0x70,
-	0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x49, 0x67, 0x6e,
-	0x6f, 0x72, 0x65, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
-	0x74, 0x73, 0x1a, 0x3c, 0x0a, 0x02, 0x4f, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x4f, 0x73, 0x54, 0x79,
-	0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x4f, 0x73, 0x54, 0x79, 0x70, 0x65,
-	0x12, 0x1e, 0x0a, 0x0a, 0x4f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02,
-	0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x4f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73,
-	0x1a, 0x43, 0x0a, 0x06, 0x4f, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x06, 0x6f, 0x73,
-	0x4c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x7a, 0x69, 0x74,
-	0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f,
-	0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4f, 0x73, 0x52, 0x06, 0x6f,
-	0x73, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x71, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73,
-	0x12, 0x16, 0x0a, 0x06, 0x4f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x06, 0x4f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06,
-	0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x48, 0x61,
-	0x73, 0x68, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72,
-	0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x46, 0x69, 0x6e, 0x67,
-	0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x70, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x63,
-	0x65, 0x73, 0x73, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6d, 0x61,
-	0x6e, 0x74, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x6d, 0x61,
-	0x6e, 0x74, 0x69, 0x63, 0x12, 0x44, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65,
-	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
-	0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75,
-	0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x52,
-	0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x73, 0x1a, 0x23, 0x0a, 0x07, 0x44, 0x6f,
-	0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73,
-	0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x1a,
-	0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
-	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30,
-	0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
-	0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62,
-	0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
-	0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x74, 0x79, 0x70, 0x65, 0x22,
-	0xe7, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e,
-	0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x38,
-	0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65,
-	0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x3a, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73,
-	0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64,
-	0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61,
-	0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04,
-	0x74, 0x61, 0x67, 0x73, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72,
-	0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
-	0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
-	0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05,
-	0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xff, 0x02, 0x0a, 0x07, 0x53, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x04, 0x74, 0x61, 0x67,
-	0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
-	0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69,
-	0x63, 0x65, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61,
-	0x67, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
-	0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12,
-	0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65,
-	0x67, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62,
-	0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x6f, 0x6c, 0x65,
-	0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f,
-	0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69,
-	0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75,
-	0x69, 0x72, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54,
-	0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x49, 0x64,
-	0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e,
-	0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65,
-	0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65,
-	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xc5, 0x02, 0x0a, 0x17,
-	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65,
-	0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x47, 0x0a, 0x04, 0x74,
-	0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x7a, 0x69, 0x74, 0x69,
-	0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72,
-	0x76, 0x69, 0x63, 0x65, 0x45, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x6f,
-	0x6c, 0x69, 0x63, 0x79, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04,
-	0x74, 0x61, 0x67, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63,
-	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63,
-	0x12, 0x28, 0x0a, 0x0f, 0x65, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f,
-	0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x65, 0x64, 0x67, 0x65, 0x52,
-	0x6f, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09,
-	0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x1a, 0x53,
+	0x65, 0x64, 0x55, 0x6e, 0x74, 0x69, 0x6c, 0x88, 0x01, 0x01, 0x1a, 0x9d, 0x01, 0x0a, 0x07, 0x45,
+	0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x41, 0x72, 0x63, 0x68, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x41, 0x72, 0x63, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x73,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73,
+	0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f,
+	0x73, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73, 0x56, 0x65,
+	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x73, 0x56,
+	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
+	0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a,
+	0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x1a, 0xa1, 0x01, 0x0a, 0x07, 0x53,
+	0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x49, 0x64, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x41, 0x70, 0x70, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a,
+	0x41, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0a, 0x41, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06,
+	0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x42, 0x72,
+	0x61, 0x6e, 0x63, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
+	0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
+	0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
+	0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x53,
 	0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
 	0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a,
 	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a,
 	0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e,
 	0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a,
-	0x02, 0x38, 0x01, 0x22, 0xfb, 0x02, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x61, 0x67,
-	0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
-	0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69,
-	0x63, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74,
-	0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6d, 0x61,
-	0x6e, 0x74, 0x69, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x6d, 0x61,
-	0x6e, 0x74, 0x69, 0x63, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79,
-	0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x64, 0x65,
-	0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x73, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09,
-	0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x2c,
-	0x0a, 0x11, 0x70, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x6f,
-	0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x70, 0x6f, 0x73, 0x74, 0x75,
-	0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a,
-	0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x0a, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x1a, 0x53, 0x0a, 0x09,
+	0x02, 0x38, 0x01, 0x1a, 0x4c, 0x0a, 0x1e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x6f,
+	0x73, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73,
+	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
+	0x01, 0x1a, 0x46, 0x0a, 0x18, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x48, 0x6f, 0x73, 0x74,
+	0x69, 0x6e, 0x67, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a,
+	0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
+	0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e,
+	0x76, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x64, 0x6b, 0x49, 0x6e, 0x66,
+	0x6f, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64,
+	0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x41, 0x74, 0x42,
+	0x10, 0x0a, 0x0e, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x55, 0x6e, 0x74, 0x69,
+	0x6c, 0x22, 0xcd, 0x01, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65, 0x6e,
+	0x74, 0x69, 0x74, 0x79, 0x57, 0x69, 0x74, 0x68, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65,
+	0x6e, 0x74, 0x73, 0x43, 0x6d, 0x64, 0x12, 0x36, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69,
+	0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e,
+	0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x49, 0x64, 0x65, 0x6e,
+	0x74, 0x69, 0x74, 0x79, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3e,
+	0x0a, 0x0b, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f,
+	0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e,
+	0x74, 0x52, 0x0b, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x31,
+	0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69,
+	0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43,
+	0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74,
+	0x78, 0x22, 0x9d, 0x02, 0x0a, 0x03, 0x4d, 0x66, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x33, 0x0a, 0x04, 0x74, 0x61, 0x67,
+	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
+	0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x66, 0x61, 0x2e, 0x54,
+	0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e,
+	0x0a, 0x0a, 0x69, 0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x1e,
+	0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x16,
+	0x0a, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
+	0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65,
+	0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72,
+	0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x73, 0x1a, 0x53, 0x0a, 0x09,
 	0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
 	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76,
 	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74,
 	0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61,
 	0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
-	0x01, 0x22, 0x8e, 0x04, 0x0a, 0x0d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75,
-	0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18,
-	0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67,
-	0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74,
-	0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
-	0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x56, 0x65, 0x72, 0x69,
-	0x66, 0x69, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x56, 0x65,
-	0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72,
-	0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x66,
-	0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a,
-	0x15, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x46, 0x69, 0x6e, 0x67, 0x65,
-	0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x15,
-	0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72,
-	0x70, 0x72, 0x69, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a, 0x11, 0x75, 0x6e, 0x76, 0x65,
-	0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x18, 0x07, 0x20,
-	0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x11, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65,
-	0x64, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x63,
-	0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12,
-	0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x18, 0x09,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, 0x65, 0x72, 0x73, 0x61,
-	0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0a, 0x20,
-	0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x1a, 0x53, 0x0a,
-	0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
-	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05,
-	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69,
-	0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54,
-	0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
-	0x38, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69,
-	0x6e, 0x74, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65,
-	0x64, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x42, 0x14, 0x0a, 0x12,
-	0x5f, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x50,
-	0x65, 0x6d, 0x22, 0xc2, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61,
-	0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x43, 0x6d, 0x64, 0x12, 0x37, 0x0a,
-	0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e,
-	0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62,
-	0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x52, 0x06,
-	0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x3c, 0x0a, 0x0a, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c,
-	0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x7a, 0x69, 0x74,
-	0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e,
-	0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c,
-	0x6d, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d,
-	0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65,
-	0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x22, 0xaa, 0x02, 0x0a, 0x17, 0x55, 0x70, 0x64, 0x61,
-	0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73,
-	0x43, 0x6d, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49,
-	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
-	0x79, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x64, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x03, 0x61, 0x64, 0x64, 0x12, 0x5f, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
-	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e,
-	0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62,
-	0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
-	0x6e, 0x66, 0x69, 0x67, 0x73, 0x43, 0x6d, 0x64, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
-	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
-	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x31, 0x0a, 0x03, 0x63, 0x74, 0x78, 0x18, 0x04, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f,
-	0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x6e,
-	0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x1a, 0x49, 0x0a, 0x0d, 0x53, 0x65, 0x72,
-	0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
-	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6e, 0x66,
-	0x69, 0x67, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x66,
-	0x69, 0x67, 0x49, 0x64, 0x2a, 0x80, 0x02, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
-	0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x1d,
-	0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x64, 0x67, 0x65, 0x54, 0x65, 0x72, 0x6d,
-	0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe8, 0x07, 0x12, 0x2b, 0x0a,
-	0x26, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65,
-	0x6e, 0x74, 0x57, 0x69, 0x74, 0x68, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61,
-	0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe9, 0x07, 0x12, 0x19, 0x0a, 0x14, 0x43, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x45, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x54, 0x79,
-	0x70, 0x65, 0x10, 0xea, 0x07, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54,
-	0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65,
-	0x10, 0xeb, 0x07, 0x12, 0x26, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x64, 0x65,
-	0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x69, 0x74, 0x68, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d,
-	0x65, 0x6e, 0x74, 0x73, 0x54, 0x79, 0x70, 0x65, 0x10, 0xec, 0x07, 0x12, 0x1d, 0x0a, 0x18, 0x55,
-	0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66,
-	0x69, 0x67, 0x73, 0x54, 0x79, 0x70, 0x65, 0x10, 0xed, 0x07, 0x12, 0x1b, 0x0a, 0x16, 0x52, 0x65,
-	0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x45, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72,
-	0x54, 0x79, 0x70, 0x65, 0x10, 0xee, 0x07, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75,
-	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x65,
-	0x64, 0x67, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x5f,
-	0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x01, 0x22, 0xa0, 0x0a, 0x0a, 0x0c, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65,
+	0x63, 0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
+	0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65,
+	0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43,
+	0x68, 0x65, 0x63, 0x6b, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04,
+	0x74, 0x61, 0x67, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x79, 0x70, 0x65, 0x49, 0x64, 0x18, 0x04,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x79, 0x70, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07,
+	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76,
+	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x74,
+	0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e,
+	0x72, 0x6f, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x36,
+	0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x7a, 0x69,
+	0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50,
+	0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4d, 0x61, 0x63, 0x48,
+	0x00, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x12, 0x36, 0x0a, 0x03, 0x6d, 0x66, 0x61, 0x18, 0x08, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f,
+	0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68,
+	0x65, 0x63, 0x6b, 0x2e, 0x4d, 0x66, 0x61, 0x48, 0x00, 0x52, 0x03, 0x6d, 0x66, 0x61, 0x12, 0x3f,
+	0x0a, 0x06, 0x6f, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25,
+	0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70,
+	0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x4f,
+	0x73, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x6f, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12,
+	0x42, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x26, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64,
+	0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b,
+	0x2e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x48, 0x00, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x63,
+	0x65, 0x73, 0x73, 0x12, 0x51, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x75,
+	0x6c, 0x74, 0x69, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x7a, 0x69, 0x74, 0x69,
+	0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73,
+	0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73,
+	0x73, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x48, 0x00, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73,
+	0x73, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x12, 0x42, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
+	0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
+	0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75,
+	0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x48,
+	0x00, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x1a, 0x29, 0x0a, 0x03, 0x4d, 0x61,
+	0x63, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65,
+	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72,
+	0x65, 0x73, 0x73, 0x65, 0x73, 0x1a, 0xaf, 0x01, 0x0a, 0x03, 0x4d, 0x66, 0x61, 0x12, 0x26, 0x0a,
+	0x0e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65,
+	0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4f,
+	0x6e, 0x57, 0x61, 0x6b, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x50, 0x72, 0x6f,
+	0x6d, 0x70, 0x74, 0x4f, 0x6e, 0x57, 0x61, 0x6b, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x50, 0x72, 0x6f,
+	0x6d, 0x70, 0x74, 0x4f, 0x6e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4f, 0x6e, 0x55, 0x6e, 0x6c, 0x6f, 0x63,
+	0x6b, 0x12, 0x34, 0x0a, 0x15, 0x49, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x4c, 0x65, 0x67, 0x61, 0x63,
+	0x79, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08,
+	0x52, 0x15, 0x49, 0x67, 0x6e, 0x6f, 0x72, 0x65, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x45, 0x6e,
+	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x3c, 0x0a, 0x02, 0x4f, 0x73, 0x12, 0x16, 0x0a,
+	0x06, 0x4f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x4f,
+	0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x4f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69,
+	0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x4f, 0x73, 0x56, 0x65, 0x72,
+	0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x43, 0x0a, 0x06, 0x4f, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12,
+	0x39, 0x0a, 0x06, 0x6f, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x21, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e,
+	0x70, 0x62, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e,
+	0x4f, 0x73, 0x52, 0x06, 0x6f, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x1a, 0x71, 0x0a, 0x07, 0x50, 0x72,
+	0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x4f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x4f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
+	0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x61, 0x74,
+	0x68, 0x12, 0x16, 0x0a, 0x06, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x06, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x46, 0x69, 0x6e,
+	0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52,
+	0x0c, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x70, 0x0a,
+	0x0c, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x12, 0x1a, 0x0a,
+	0x08, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x08, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x12, 0x44, 0x0a, 0x09, 0x70, 0x72, 0x6f,
+	0x63, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a,
+	0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e,
+	0x50, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2e, 0x50, 0x72, 0x6f,
+	0x63, 0x65, 0x73, 0x73, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x73, 0x1a,
+	0x23, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f,
+	0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d,
+	0x61, 0x69, 0x6e, 0x73, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
+	0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
+	0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x73, 0x75, 0x62,
+	0x74, 0x79, 0x70, 0x65, 0x22, 0xe7, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74,
+	0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x02, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+	0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x3a, 0x0a,
+	0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x7a, 0x69,
+	0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x52,
+	0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e,
+	0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67,
+	0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
+	0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
+	0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61,
+	0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xff,
+	0x02, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x37,
+	0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x7a,
+	0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e,
+	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69,
+	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53,
+	0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x41,
+	0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52,
+	0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12,
+	0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09,
+	0x52, 0x07, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6e, 0x63,
+	0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18,
+	0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f,
+	0x6e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x61, 0x78,
+	0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
+	0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x53, 0x0a, 0x09, 0x54,
+	0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61,
+	0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69,
+	0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67,
+	0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
+	0x22, 0xc5, 0x02, 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x64, 0x67, 0x65,
+	0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x0e, 0x0a, 0x02,
+	0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x12, 0x47, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33,
+	0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70,
+	0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75,
+	0x74, 0x65, 0x72, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e,
+	0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6d,
+	0x61, 0x6e, 0x74, 0x69, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x6d,
+	0x61, 0x6e, 0x74, 0x69, 0x63, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75,
+	0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f,
+	0x65, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12,
+	0x22, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18,
+	0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f,
+	0x6c, 0x65, 0x73, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
+	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
+	0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d,
+	0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76,
+	0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xfb, 0x02, 0x0a, 0x0d, 0x53, 0x65, 0x72,
+	0x76, 0x69, 0x63, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3d,
+	0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x7a,
+	0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e,
+	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x54, 0x61,
+	0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1a, 0x0a,
+	0x08, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x08, 0x73, 0x65, 0x6d, 0x61, 0x6e, 0x74, 0x69, 0x63, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x64, 0x65,
+	0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09,
+	0x52, 0x0d, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12,
+	0x22, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18,
+	0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x6f,
+	0x6c, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x70, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68,
+	0x65, 0x63, 0x6b, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11,
+	0x70, 0x6f, 0x73, 0x74, 0x75, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x6f, 0x6c, 0x65,
+	0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x18,
+	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70,
+	0x65, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
+	0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
+	0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e,
+	0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c,
+	0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8e, 0x04, 0x0a, 0x0d, 0x54, 0x72, 0x61, 0x6e, 0x73,
+	0x69, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x04,
+	0x74, 0x61, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x7a, 0x69, 0x74,
+	0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x72,
+	0x61, 0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2e, 0x54, 0x61, 0x67, 0x73,
+	0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69,
+	0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52,
+	0x0a, 0x69, 0x73, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0b, 0x66,
+	0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
+	0x48, 0x00, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x88,
+	0x01, 0x01, 0x12, 0x39, 0x0a, 0x15, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64,
+	0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28,
+	0x09, 0x48, 0x01, 0x52, 0x15, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x46,
+	0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x31, 0x0a,
+	0x11, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x50,
+	0x65, 0x6d, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x11, 0x75, 0x6e, 0x76, 0x65,
+	0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x88, 0x01, 0x01,
+	0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04,
+	0x63, 0x6f, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61, 0x76, 0x65, 0x72,
+	0x73, 0x61, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6e, 0x6f, 0x54, 0x72, 0x61,
+	0x76, 0x65, 0x72, 0x73, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
+	0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
+	0x65, 0x64, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
+	0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
+	0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64,
+	0x2e, 0x70, 0x62, 0x2e, 0x54, 0x61, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61,
+	0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x66, 0x69, 0x6e, 0x67,
+	0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x75, 0x6e, 0x76, 0x65,
+	0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x46, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e,
+	0x74, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x75, 0x6e, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64,
+	0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x22, 0xc2, 0x01, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61,
+	0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x43,
+	0x6d, 0x64, 0x12, 0x37, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
+	0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75,
+	0x74, 0x65, 0x72, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x3c, 0x0a, 0x0a, 0x65,
+	0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1c, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e,
+	0x70, 0x62, 0x2e, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x0a, 0x65,
+	0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x31, 0x0a, 0x03, 0x63, 0x74, 0x78,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64,
+	0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
+	0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x22, 0xaa, 0x02, 0x0a,
+	0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
+	0x6e, 0x66, 0x69, 0x67, 0x73, 0x43, 0x6d, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e,
+	0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64,
+	0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x64, 0x64, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x64, 0x64, 0x12, 0x5f, 0x0a, 0x0e, 0x73, 0x65,
+	0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03,
+	0x28, 0x0b, 0x32, 0x37, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
+	0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76,
+	0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x43, 0x6d, 0x64, 0x2e, 0x53, 0x65,
+	0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x73, 0x65, 0x72,
+	0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x31, 0x0a, 0x03, 0x63,
+	0x74, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e,
+	0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x6d, 0x64, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x68, 0x61, 0x6e,
+	0x67, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x52, 0x03, 0x63, 0x74, 0x78, 0x1a, 0x49,
+	0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
+	0x1c, 0x0a, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a,
+	0x08, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x08, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x49, 0x64, 0x2a, 0x80, 0x02, 0x0a, 0x0b, 0x43, 0x6f,
+	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x5a, 0x65, 0x72,
+	0x6f, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x64, 0x67,
+	0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x10,
+	0xe8, 0x07, 0x12, 0x2b, 0x0a, 0x26, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x45, 0x6e, 0x72,
+	0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x57, 0x69, 0x74, 0x68, 0x41, 0x75, 0x74, 0x68, 0x65,
+	0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe9, 0x07, 0x12,
+	0x19, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x64, 0x67, 0x65, 0x52, 0x6f, 0x75,
+	0x74, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xea, 0x07, 0x12, 0x1c, 0x0a, 0x17, 0x43, 0x72,
+	0x65, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65,
+	0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xeb, 0x07, 0x12, 0x26, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61,
+	0x74, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x57, 0x69, 0x74, 0x68, 0x45, 0x6e,
+	0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x54, 0x79, 0x70, 0x65, 0x10, 0xec, 0x07,
+	0x12, 0x1d, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+	0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x54, 0x79, 0x70, 0x65, 0x10, 0xed, 0x07, 0x12,
+	0x1b, 0x0a, 0x16, 0x52, 0x65, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x45, 0x64, 0x67, 0x65, 0x52,
+	0x6f, 0x75, 0x74, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xee, 0x07, 0x42, 0x29, 0x5a, 0x27,
+	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a,
+	0x69, 0x74, 0x69, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x64, 0x67, 0x65,
+	0x5f, 0x63, 0x6d, 0x64, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/common/pb/edge_cmd_pb/edge_cmd.proto b/common/pb/edge_cmd_pb/edge_cmd.proto
index b71b86ab4..e12d6f7e5 100644
--- a/common/pb/edge_cmd_pb/edge_cmd.proto
+++ b/common/pb/edge_cmd_pb/edge_cmd.proto
@@ -256,6 +256,8 @@ message Identity {
     string Os = 2;
     string OsRelease = 3;
     string OsVersion = 4;
+    string Domain = 5;
+    string Hostname = 6;
   }
 
   message SdkInfo {
diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go
index bee4f94d5..a50e159cc 100644
--- a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go
+++ b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v3.21.12
+// 	protoc        v4.23.4
 // source: edge_ctrl.proto
 
 package edge_ctrl_pb
@@ -2330,6 +2330,8 @@ type EnvInfo struct {
 	Os        string `protobuf:"bytes,2,opt,name=Os,proto3" json:"Os,omitempty"`
 	OsRelease string `protobuf:"bytes,3,opt,name=OsRelease,proto3" json:"OsRelease,omitempty"`
 	OsVersion string `protobuf:"bytes,4,opt,name=OsVersion,proto3" json:"OsVersion,omitempty"`
+	Hostname  string `protobuf:"bytes,5,opt,name=Hostname,proto3" json:"Hostname,omitempty"`
+	Domain    string `protobuf:"bytes,6,opt,name=Domain,proto3" json:"Domain,omitempty"`
 }
 
 func (x *EnvInfo) Reset() {
@@ -2392,6 +2394,20 @@ func (x *EnvInfo) GetOsVersion() string {
 	return ""
 }
 
+func (x *EnvInfo) GetHostname() string {
+	if x != nil {
+		return x.Hostname
+	}
+	return ""
+}
+
+func (x *EnvInfo) GetDomain() string {
+	if x != nil {
+		return x.Domain
+	}
+	return ""
+}
+
 type SdkInfo struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -3760,350 +3776,353 @@ var file_edge_ctrl_proto_rawDesc = []byte{
 	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
 	0x24, 0x0a, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73,
 	0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54,
-	0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x69, 0x0a, 0x07, 0x45, 0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f,
-	0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
-	0x61, 0x72, 0x63, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x02, 0x4f, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73,
-	0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x73, 0x52, 0x65, 0x6c, 0x65, 0x61,
-	0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
-	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
-	0x22, 0xa1, 0x01, 0x0a, 0x07, 0x53, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05,
-	0x41, 0x70, 0x70, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x41, 0x70, 0x70,
-	0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x41, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x41, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69,
-	0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x06, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x52, 0x65,
-	0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x52, 0x65,
-	0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x05,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65,
-	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72,
-	0x73, 0x69, 0x6f, 0x6e, 0x22, 0xa7, 0x01, 0x0a, 0x17, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41,
-	0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x12, 0x34, 0x0a, 0x07, 0x65, 0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74,
-	0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x65,
-	0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x34, 0x0a, 0x07, 0x73, 0x64, 0x6b, 0x49, 0x6e, 0x66,
-	0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
-	0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x53, 0x64, 0x6b, 0x49,
-	0x6e, 0x66, 0x6f, 0x52, 0x07, 0x73, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x20, 0x0a, 0x0b,
-	0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
-	0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0x8a,
-	0x06, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73,
-	0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73,
-	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
-	0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b,
-	0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12,
-	0x36, 0x0a, 0x16, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76,
-	0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52,
-	0x16, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c,
-	0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74,
-	0x69, 0x74, 0x79, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x64, 0x65,
-	0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74,
-	0x69, 0x74, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69,
-	0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x63, 0x0a, 0x18, 0x64,
-	0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x65,
-	0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e,
-	0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70,
-	0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63,
-	0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x18, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48,
-	0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65,
-	0x12, 0x2e, 0x0a, 0x12, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x69,
-	0x6e, 0x67, 0x43, 0x6f, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x64, 0x65,
-	0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x73, 0x74,
-	0x12, 0x20, 0x0a, 0x0b, 0x61, 0x70, 0x70, 0x44, 0x61, 0x74, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x18,
-	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x70, 0x44, 0x61, 0x74, 0x61, 0x4a, 0x73,
-	0x6f, 0x6e, 0x12, 0x73, 0x0a, 0x12, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x65,
-	0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43,
-	0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e,
-	0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73,
-	0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76,
-	0x69, 0x63, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x45, 0x6e,
-	0x74, 0x72, 0x79, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x65, 0x63,
-	0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69,
-	0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e,
-	0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70,
-	0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69,
-	0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69,
-	0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x73, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x1a, 0x6e, 0x0a, 0x17, 0x53, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73,
-	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64,
-	0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69,
-	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52,
-	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3f, 0x0a, 0x11, 0x53, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
-	0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
-	0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
-	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xae, 0x02, 0x0a, 0x1e,
-	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72,
-	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c,
-	0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0b,
-	0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32,
-	0x0a, 0x14, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x73, 0x74,
-	0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x74, 0x65,
-	0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65,
-	0x49, 0x64, 0x12, 0x5b, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x04,
-	0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65,
-	0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
-	0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61,
-	0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x1a,
-	0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79,
-	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b,
-	0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x4b, 0x0a, 0x15,
-	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
-	0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f,
-	0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x90, 0x04, 0x0a, 0x1f, 0x43, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a,
-	0x0a, 0x61, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x2b, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74,
-	0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53,
-	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a,
-	0x61, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x07, 0x73, 0x65,
-	0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x7a, 0x69,
+	0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x22, 0x9d, 0x01, 0x0a, 0x07, 0x45, 0x6e, 0x76, 0x49, 0x6e, 0x66,
+	0x6f, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x04, 0x61, 0x72, 0x63, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x02, 0x4f, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73, 0x52, 0x65, 0x6c, 0x65, 0x61,
+	0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x73, 0x52, 0x65, 0x6c, 0x65,
+	0x61, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
+	0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x08, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a,
+	0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44,
+	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0xa1, 0x01, 0x0a, 0x07, 0x53, 0x64, 0x6b, 0x49, 0x6e, 0x66,
+	0x6f, 0x12, 0x14, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x05, 0x41, 0x70, 0x70, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x41, 0x70, 0x70, 0x56, 0x65,
+	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x41, 0x70, 0x70,
+	0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x42, 0x72, 0x61, 0x6e, 0x63,
+	0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x42, 0x72, 0x61, 0x6e, 0x63, 0x68, 0x12,
+	0x1a, 0x0a, 0x08, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x08, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x54,
+	0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
+	0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xa7, 0x01, 0x0a, 0x17, 0x43, 0x72,
+	0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x65, 0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64,
+	0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x45, 0x6e, 0x76, 0x49, 0x6e,
+	0x66, 0x6f, 0x52, 0x07, 0x65, 0x6e, 0x76, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x34, 0x0a, 0x07, 0x73,
+	0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x7a,
+	0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62,
+	0x2e, 0x53, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x07, 0x73, 0x64, 0x6b, 0x49, 0x6e, 0x66,
+	0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x79, 0x70, 0x65, 0x73,
+	0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x79,
+	0x70, 0x65, 0x73, 0x22, 0x8a, 0x06, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70,
+	0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14,
+	0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74,
+	0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49,
+	0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x03,
+	0x20, 0x01, 0x28, 0x0d, 0x52, 0x16, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74,
+	0x65, 0x72, 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x1e, 0x0a, 0x0a,
+	0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c,
+	0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x4e, 0x61, 0x6d, 0x65,
+	0x12, 0x63, 0x0a, 0x18, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x69,
+	0x6e, 0x67, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01,
+	0x28, 0x0e, 0x32, 0x27, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
+	0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x18, 0x64, 0x65, 0x66,
+	0x61, 0x75, 0x6c, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x72, 0x65, 0x63, 0x65,
+	0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
+	0x48, 0x6f, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28,
+	0x0d, 0x52, 0x12, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x69, 0x6e,
+	0x67, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x70, 0x70, 0x44, 0x61, 0x74, 0x61,
+	0x4a, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x70, 0x44,
+	0x61, 0x74, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x73, 0x0a, 0x12, 0x73, 0x65, 0x72, 0x76, 0x69,
+	0x63, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x09, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f,
+	0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70,
+	0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+	0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e,
+	0x63, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
+	0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x0c,
+	0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03,
+	0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
+	0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69,
+	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e,
+	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x1a,
+	0x6e, 0x0a, 0x17, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64,
+	0x65, 0x6e, 0x63, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
+	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3d, 0x0a, 0x05,
+	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x7a, 0x69,
 	0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e,
-	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c,
-	0x0a, 0x09, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x09, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07,
-	0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61,
-	0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x5c, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61,
-	0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e,
-	0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65,
-	0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72,
-	0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x50, 0x65, 0x65,
+	0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64,
+	0x65, 0x6e, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a,
+	0x3f, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x45,
+	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
+	0x22, 0xae, 0x02, 0x0a, 0x1e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75,
+	0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49,
+	0x64, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e,
+	0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x14, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x73,
+	0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x5b, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44,
+	0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x7a, 0x69, 0x74, 0x69,
+	0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72,
+	0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65,
+	0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x65, 0x65,
 	0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72,
-	0x44, 0x61, 0x74, 0x61, 0x12, 0x50, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03,
-	0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
-	0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72,
-	0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
-	0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61,
-	0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c,
-	0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a,
-	0x02, 0x38, 0x01, 0x1a, 0x37, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
-	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
-	0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6c, 0x0a, 0x0c,
-	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a,
-	0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
-	0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x08,
-	0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20,
-	0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e,
-	0x70, 0x62, 0x2e, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
-	0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0xa1, 0x01, 0x0a, 0x0d, 0x54,
-	0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02,
-	0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04,
-	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
-	0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18,
-	0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f,
-	0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e,
-	0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69,
-	0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61,
-	0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x22, 0xd5,
-	0x03, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54,
-	0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61,
-	0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64,
-	0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x5a, 0x0a, 0x08, 0x70, 0x65,
-	0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x7a,
-	0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62,
-	0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72,
-	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x50,
-	0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x70, 0x65,
-	0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x05,
-	0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x0a, 0x70, 0x72,
-	0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27,
-	0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e,
-	0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65,
-	0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65,
-	0x6e, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49,
-	0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63,
-	0x65, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53,
-	0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x69, 0x6e, 0x73,
-	0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73,
-	0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
-	0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65,
-	0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
-	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
-	0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c,
-	0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf3, 0x01, 0x0a, 0x1e, 0x43, 0x72, 0x65, 0x61, 0x74,
-	0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
-	0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x61, 0x70, 0x69,
-	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e,
+	0x44, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61,
+	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
+	0x01, 0x22, 0x4b, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69,
+	0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65,
+	0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73,
+	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65,
+	0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x90,
+	0x04, 0x0a, 0x1f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74,
+	0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x61, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64,
+	0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x52, 0x0a, 0x61, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12,
+	0x42, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x28, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72,
+	0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69,
+	0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73,
+	0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x49, 0x64,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x49,
+	0x64, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x5c, 0x0a, 0x08, 0x70,
+	0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e,
 	0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70,
-	0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69,
-	0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x61, 0x70, 0x69, 0x53,
-	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f,
-	0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
+	0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46,
+	0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
+	0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x50, 0x0a, 0x04, 0x74, 0x61, 0x67,
+	0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65,
 	0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61,
-	0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
-	0x65, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x65,
-	0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x1c,
-	0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xec, 0x01, 0x0a,
-	0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72,
-	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22,
-	0x0a, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
-	0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
-	0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64,
-	0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x7a, 0x69, 0x74,
-	0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54,
-	0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65,
-	0x6e, 0x63, 0x65, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12,
-	0x2a, 0x0a, 0x10, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65,
-	0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x75, 0x70, 0x64, 0x61, 0x74,
-	0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x75,
-	0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52,
-	0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x22, 0x9d, 0x01, 0x0a, 0x1d,
-	0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64,
-	0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a,
-	0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x43, 0x73, 0x72, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74,
-	0x43, 0x73, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x65, 0x72,
-	0x74, 0x43, 0x73, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x72, 0x76,
-	0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x43, 0x73, 0x72, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x65, 0x71,
-	0x75, 0x69, 0x72, 0x65, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x56,
-	0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x85, 0x01, 0x0a, 0x17,
-	0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x73, 0x52,
-	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e,
-	0x74, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
-	0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x12, 0x24, 0x0a,
-	0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74,
-	0x50, 0x65, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x43, 0x65, 0x72, 0x74, 0x73, 0x50, 0x65,
-	0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61, 0x43, 0x65, 0x72, 0x74, 0x73,
-	0x50, 0x65, 0x6d, 0x22, 0x4b, 0x0a, 0x23, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e,
-	0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x56, 0x65, 0x72,
-	0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6c,
-	0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d,
-	0x2a, 0xf4, 0x0a, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65,
-	0x12, 0x08, 0x0a, 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x0f, 0x53, 0x65,
-	0x72, 0x76, 0x65, 0x72, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x10, 0xa0, 0x9c,
-	0x01, 0x12, 0x15, 0x0a, 0x0f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
-	0x54, 0x79, 0x70, 0x65, 0x10, 0xa1, 0x9c, 0x01, 0x12, 0x0f, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f,
-	0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xa2, 0x9c, 0x01, 0x12, 0x18, 0x0a, 0x12, 0x53, 0x65, 0x73,
-	0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x10,
-	0x86, 0x9d, 0x01, 0x12, 0x19, 0x0a, 0x13, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
-	0x6e, 0x41, 0x64, 0x64, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe8, 0x9d, 0x01, 0x12, 0x1b,
-	0x0a, 0x15, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x55, 0x70, 0x64, 0x61,
-	0x74, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe9, 0x9d, 0x01, 0x12, 0x1b, 0x0a, 0x15, 0x41,
-	0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64,
-	0x54, 0x79, 0x70, 0x65, 0x10, 0xea, 0x9d, 0x01, 0x12, 0x1d, 0x0a, 0x17, 0x41, 0x70, 0x69, 0x53,
-	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x54,
-	0x79, 0x70, 0x65, 0x10, 0xeb, 0x9d, 0x01, 0x12, 0x1d, 0x0a, 0x17, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x53, 0x79, 0x6e, 0x63, 0x54, 0x79,
-	0x70, 0x65, 0x10, 0xec, 0x9d, 0x01, 0x12, 0x1e, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79,
-	0x70, 0x65, 0x10, 0xed, 0x9d, 0x01, 0x12, 0x1f, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54,
-	0x79, 0x70, 0x65, 0x10, 0xee, 0x9d, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74,
-	0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xef, 0x9d, 0x01, 0x12, 0x22, 0x0a, 0x1c, 0x43, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf0, 0x9d, 0x01, 0x12, 0x21,
-	0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
-	0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf1, 0x9d,
-	0x01, 0x12, 0x22, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69,
-	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70,
-	0x65, 0x10, 0xf2, 0x9d, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54,
-	0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x54, 0x79, 0x70, 0x65, 0x10, 0xf3, 0x9d, 0x01, 0x12, 0x22, 0x0a, 0x1c, 0x52, 0x65, 0x6d, 0x6f,
-	0x76, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf4, 0x9d, 0x01, 0x12, 0x21, 0x0a, 0x1b,
-	0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf5, 0x9d, 0x01, 0x12,
-	0x15, 0x0a, 0x0f, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79,
-	0x70, 0x65, 0x10, 0xf6, 0x9d, 0x01, 0x12, 0x1a, 0x0a, 0x14, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e,
-	0x67, 0x43, 0x65, 0x72, 0x74, 0x41, 0x64, 0x64, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf7,
-	0x9d, 0x01, 0x12, 0x23, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d,
-	0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x56, 0x32, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54,
-	0x79, 0x70, 0x65, 0x10, 0xf8, 0x9d, 0x01, 0x12, 0x24, 0x0a, 0x1e, 0x43, 0x72, 0x65, 0x61, 0x74,
-	0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x56, 0x32, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf9, 0x9d, 0x01, 0x12, 0x20, 0x0a,
-	0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x56, 0x32,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xfa, 0x9d, 0x01, 0x12,
-	0x21, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74,
-	0x56, 0x32, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xfb,
-	0x9d, 0x01, 0x12, 0x10, 0x0a, 0x0a, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x54, 0x79, 0x70, 0x65,
-	0x10, 0xcc, 0x9e, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65,
-	0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54,
-	0x79, 0x70, 0x65, 0x10, 0xcd, 0x9e, 0x01, 0x12, 0x27, 0x0a, 0x21, 0x45, 0x6e, 0x72, 0x6f, 0x6c,
-	0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65,
-	0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xce, 0x9e, 0x01,
-	0x12, 0x2d, 0x0a, 0x27, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x78,
-	0x74, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xcf, 0x9e, 0x01, 0x12,
-	0x21, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73,
-	0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb0,
-	0x9f, 0x01, 0x12, 0x22, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69, 0x53,
-	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79,
-	0x70, 0x65, 0x10, 0xb1, 0x9f, 0x01, 0x12, 0x28, 0x0a, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
-	0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
-	0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb2, 0x9f, 0x01,
-	0x12, 0x29, 0x0a, 0x23, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69,
-	0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb3, 0x9f, 0x01, 0x12, 0x1d, 0x0a, 0x17, 0x4c,
-	0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb4, 0x9f, 0x01, 0x12, 0x15, 0x0a, 0x0f, 0x53, 0x65,
-	0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb5, 0x9f,
-	0x01, 0x12, 0x27, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65,
-	0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb6, 0x9f, 0x01, 0x12, 0x28, 0x0a, 0x22, 0x43, 0x72,
-	0x65, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e,
-	0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65,
-	0x10, 0xb7, 0x9f, 0x01, 0x12, 0x27, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75,
+	0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76,
+	0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x54, 0x61, 0x67, 0x73,
+	0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x50,
+	0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
+	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14,
+	0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76,
+	0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x37, 0x0a, 0x09, 0x54, 0x61, 0x67, 0x73,
+	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
+	0x01, 0x22, 0x6c, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x4c, 0x69, 0x73,
+	0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74,
+	0x65, 0x12, 0x3c, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f,
+	0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x53, 0x65,
+	0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22,
+	0xa1, 0x01, 0x0a, 0x0d, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+	0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
+	0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
+	0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d,
+	0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79,
+	0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x65, 0x6e, 0x63,
+	0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69,
+	0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
+	0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74,
+	0x61, 0x67, 0x73, 0x22, 0xd5, 0x03, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x75,
 	0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb8, 0x9f, 0x01, 0x12, 0x28, 0x0a,
-	0x22, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72,
-	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54,
-	0x79, 0x70, 0x65, 0x10, 0xb9, 0x9f, 0x01, 0x12, 0x27, 0x0a, 0x21, 0x52, 0x65, 0x6d, 0x6f, 0x76,
-	0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
-	0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xba, 0x9f, 0x01,
-	0x12, 0x28, 0x0a, 0x22, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76,
+	0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69,
+	0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73,
+	0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
+	0x5a, 0x0a, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x3e, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74,
+	0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e,
+	0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x52, 0x08, 0x70, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x63,
+	0x6f, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12,
+	0x47, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20,
+	0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f,
+	0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
+	0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x70, 0x72,
+	0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x73, 0x74,
+	0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e,
+	0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74,
+	0x61, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
+	0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20,
+	0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x1a, 0x3b,
+	0x0a, 0x0d, 0x50, 0x65, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
+	0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65,
+	0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xf3, 0x01, 0x0a, 0x1e,
+	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d,
+	0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b,
+	0x0a, 0x0a, 0x61, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63,
+	0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70, 0x69,
+	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52,
+	0x0a, 0x61, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x42, 0x0a, 0x07, 0x73,
+	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x7a,
+	0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62,
+	0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x07, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12,
+	0x22, 0x0a, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d,
+	0x65, 0x22, 0xec, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e,
+	0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69,
+	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x73, 0x74, 0x12, 0x47, 0x0a, 0x0a, 0x70,
+	0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32,
+	0x27, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c,
+	0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72,
+	0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64,
+	0x65, 0x6e, 0x63, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72,
+	0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10,
+	0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63, 0x65,
+	0x12, 0x1e, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x18, 0x05,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x73, 0x74,
+	0x22, 0x9d, 0x01, 0x0a, 0x1d, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x45,
+	0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74,
+	0x43, 0x73, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e,
+	0x74, 0x43, 0x65, 0x72, 0x74, 0x43, 0x73, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76,
+	0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x43, 0x73, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x43, 0x73, 0x72, 0x12, 0x30,
+	0x0a, 0x13, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x65, 0x71,
+	0x75, 0x69, 0x72, 0x65, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x22, 0x85, 0x01, 0x0a, 0x17, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x43,
+	0x65, 0x72, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d,
+	0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x50,
+	0x65, 0x6d, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74,
+	0x50, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x65,
+	0x72, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x43, 0x65,
+	0x72, 0x74, 0x73, 0x50, 0x65, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x61,
+	0x43, 0x65, 0x72, 0x74, 0x73, 0x50, 0x65, 0x6d, 0x22, 0x4b, 0x0a, 0x23, 0x45, 0x6e, 0x72, 0x6f,
+	0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74,
+	0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+	0x24, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x50, 0x65, 0x6d,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x65,
+	0x72, 0x74, 0x50, 0x65, 0x6d, 0x2a, 0xf4, 0x0a, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+	0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12,
+	0x15, 0x0a, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x54, 0x79,
+	0x70, 0x65, 0x10, 0xa0, 0x9c, 0x01, 0x12, 0x15, 0x0a, 0x0f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
+	0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x10, 0xa1, 0x9c, 0x01, 0x12, 0x0f, 0x0a,
+	0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x10, 0xa2, 0x9c, 0x01, 0x12, 0x18,
+	0x0a, 0x12, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64,
+	0x54, 0x79, 0x70, 0x65, 0x10, 0x86, 0x9d, 0x01, 0x12, 0x19, 0x0a, 0x13, 0x41, 0x70, 0x69, 0x53,
+	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x64, 0x64, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x10,
+	0xe8, 0x9d, 0x01, 0x12, 0x1b, 0x0a, 0x15, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
+	0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe9, 0x9d, 0x01,
+	0x12, 0x1b, 0x0a, 0x15, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65,
+	0x6d, 0x6f, 0x76, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x10, 0xea, 0x9d, 0x01, 0x12, 0x1d, 0x0a,
+	0x17, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x61, 0x72, 0x74,
+	0x62, 0x65, 0x61, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xeb, 0x9d, 0x01, 0x12, 0x1d, 0x0a, 0x17,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x53,
+	0x79, 0x6e, 0x63, 0x54, 0x79, 0x70, 0x65, 0x10, 0xec, 0x9d, 0x01, 0x12, 0x1e, 0x0a, 0x18, 0x43,
+	0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xed, 0x9d, 0x01, 0x12, 0x1f, 0x0a, 0x19, 0x43,
+	0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xee, 0x9d, 0x01, 0x12, 0x21, 0x0a, 0x1b,
+	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xef, 0x9d, 0x01, 0x12,
+	0x22, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
+	0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10,
+	0xf0, 0x9d, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72,
+	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79,
+	0x70, 0x65, 0x10, 0xf1, 0x9d, 0x01, 0x12, 0x22, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
 	0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbb, 0x9f, 0x01, 0x12, 0x1b, 0x0a, 0x15, 0x54, 0x75,
-	0x6e, 0x6e, 0x65, 0x6c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54,
-	0x79, 0x70, 0x65, 0x10, 0xbc, 0x9f, 0x01, 0x2a, 0x21, 0x0a, 0x0b, 0x53, 0x65, 0x73, 0x73, 0x69,
-	0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x69, 0x61, 0x6c, 0x10, 0x00,
-	0x12, 0x08, 0x0a, 0x04, 0x42, 0x69, 0x6e, 0x64, 0x10, 0x01, 0x2a, 0x3d, 0x0a, 0x06, 0x48, 0x65,
-	0x61, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x0a, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5a, 0x65,
-	0x72, 0x6f, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
-	0x6f, 0x72, 0x49, 0x64, 0x10, 0xfe, 0x07, 0x12, 0x10, 0x0a, 0x0b, 0x43, 0x68, 0x65, 0x63, 0x6b,
-	0x50, 0x61, 0x73, 0x73, 0x65, 0x64, 0x10, 0xff, 0x07, 0x2a, 0x2e, 0x0a, 0x0a, 0x43, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x64, 0x64, 0x10, 0x00,
-	0x12, 0x0a, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07,
-	0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x10, 0x02, 0x2a, 0x3d, 0x0a, 0x14, 0x54, 0x65, 0x72,
-	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63, 0x65, 0x64, 0x65, 0x6e, 0x63,
-	0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x10, 0x00, 0x12, 0x0c,
-	0x0a, 0x08, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06,
-	0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x5c, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61,
-	0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75,
-	0x6c, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x10, 0x00, 0x12,
-	0x14, 0x0a, 0x10, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x49, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x6c,
-	0x69, 0x63, 0x74, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x4f,
-	0x74, 0x68, 0x65, 0x72, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64,
-	0x42, 0x75, 0x73, 0x79, 0x10, 0x03, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
-	0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x65, 0x64,
-	0x67, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x5f,
-	0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf2, 0x9d, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x52, 0x65,
+	0x6d, 0x6f, 0x76, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf3, 0x9d, 0x01, 0x12, 0x22, 0x0a,
+	0x1c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf4, 0x9d,
+	0x01, 0x12, 0x21, 0x0a, 0x1b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x73,
+	0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65,
+	0x10, 0xf5, 0x9d, 0x01, 0x12, 0x15, 0x0a, 0x0f, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x45, 0x76,
+	0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf6, 0x9d, 0x01, 0x12, 0x1a, 0x0a, 0x14, 0x53,
+	0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x65, 0x72, 0x74, 0x41, 0x64, 0x64, 0x65, 0x64, 0x54,
+	0x79, 0x70, 0x65, 0x10, 0xf7, 0x9d, 0x01, 0x12, 0x23, 0x0a, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x56, 0x32, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf8, 0x9d, 0x01, 0x12, 0x24, 0x0a, 0x1e,
+	0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
+	0x56, 0x32, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf9,
+	0x9d, 0x01, 0x12, 0x20, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63,
+	0x75, 0x69, 0x74, 0x56, 0x32, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65,
+	0x10, 0xfa, 0x9d, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69,
+	0x72, 0x63, 0x75, 0x69, 0x74, 0x56, 0x32, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54,
+	0x79, 0x70, 0x65, 0x10, 0xfb, 0x9d, 0x01, 0x12, 0x10, 0x0a, 0x0a, 0x45, 0x6e, 0x72, 0x6f, 0x6c,
+	0x6c, 0x54, 0x79, 0x70, 0x65, 0x10, 0xcc, 0x9e, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x45, 0x6e, 0x72,
+	0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xcd, 0x9e, 0x01, 0x12, 0x27, 0x0a, 0x21,
+	0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64,
+	0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70,
+	0x65, 0x10, 0xce, 0x9e, 0x01, 0x12, 0x2d, 0x0a, 0x27, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d,
+	0x65, 0x6e, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x56,
+	0x65, 0x72, 0x69, 0x66, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65,
+	0x10, 0xcf, 0x9e, 0x01, 0x12, 0x21, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x70,
+	0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54,
+	0x79, 0x70, 0x65, 0x10, 0xb0, 0x9f, 0x01, 0x12, 0x22, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74,
+	0x65, 0x41, 0x70, 0x69, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb1, 0x9f, 0x01, 0x12, 0x28, 0x0a, 0x22, 0x43,
+	0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53,
+	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70,
+	0x65, 0x10, 0xb2, 0x9f, 0x01, 0x12, 0x29, 0x0a, 0x23, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
+	0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb3, 0x9f, 0x01,
+	0x12, 0x1d, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb4, 0x9f, 0x01, 0x12,
+	0x15, 0x0a, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x54, 0x79,
+	0x70, 0x65, 0x10, 0xb5, 0x9f, 0x01, 0x12, 0x27, 0x0a, 0x21, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
+	0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb6, 0x9f, 0x01, 0x12,
+	0x28, 0x0a, 0x22, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54,
+	0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb7, 0x9f, 0x01, 0x12, 0x27, 0x0a, 0x21, 0x55, 0x70, 0x64,
+	0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
+	0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb8,
+	0x9f, 0x01, 0x12, 0x28, 0x0a, 0x22, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x75, 0x6e, 0x6e,
+	0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb9, 0x9f, 0x01, 0x12, 0x27, 0x0a, 0x21,
+	0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d,
+	0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70,
+	0x65, 0x10, 0xba, 0x9f, 0x01, 0x12, 0x28, 0x0a, 0x22, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x54,
+	0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbb, 0x9f, 0x01, 0x12,
+	0x1b, 0x0a, 0x15, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x45,
+	0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbc, 0x9f, 0x01, 0x2a, 0x21, 0x0a, 0x0b,
+	0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x44,
+	0x69, 0x61, 0x6c, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x42, 0x69, 0x6e, 0x64, 0x10, 0x01, 0x2a,
+	0x3d, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x0a, 0x48, 0x65, 0x61,
+	0x64, 0x65, 0x72, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0c, 0x54, 0x65, 0x72,
+	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x10, 0xfe, 0x07, 0x12, 0x10, 0x0a, 0x0b,
+	0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x61, 0x73, 0x73, 0x65, 0x64, 0x10, 0xff, 0x07, 0x2a, 0x2e,
+	0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03,
+	0x41, 0x64, 0x64, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x10,
+	0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x10, 0x02, 0x2a, 0x3d,
+	0x0a, 0x14, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63,
+	0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c,
+	0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x10,
+	0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x5c, 0x0a,
+	0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x75, 0x63, 0x63, 0x65,
+	0x73, 0x73, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x49, 0x64,
+	0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x61,
+	0x69, 0x6c, 0x65, 0x64, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x46,
+	0x61, 0x69, 0x6c, 0x65, 0x64, 0x42, 0x75, 0x73, 0x79, 0x10, 0x03, 0x42, 0x2a, 0x5a, 0x28, 0x67,
+	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a, 0x69,
+	0x74, 0x69, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x5f,
+	0x63, 0x74, 0x72, 0x6c, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.proto b/common/pb/edge_ctrl_pb/edge_ctrl.proto
index c2512ad8a..58cbe9b77 100644
--- a/common/pb/edge_ctrl_pb/edge_ctrl.proto
+++ b/common/pb/edge_ctrl_pb/edge_ctrl.proto
@@ -284,6 +284,8 @@ message EnvInfo  {
   string Os = 2;
   string OsRelease = 3;
   string OsVersion = 4;
+  string Hostname = 5;
+  string Domain = 6;
 }
 
 message SdkInfo {
diff --git a/common/pb/edge_mgmt_pb/edge_mgmt.pb.go b/common/pb/edge_mgmt_pb/edge_mgmt.pb.go
index b8fa212d6..ffd8fbb8c 100644
--- a/common/pb/edge_mgmt_pb/edge_mgmt.pb.go
+++ b/common/pb/edge_mgmt_pb/edge_mgmt.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v3.21.12
+// 	protoc        v4.23.4
 // source: edge_mgmt.proto
 
 package edge_mgmt_pb
diff --git a/common/pb/mgmt_pb/mgmt.pb.go b/common/pb/mgmt_pb/mgmt.pb.go
index 2af6cd8ad..76c9a8b29 100644
--- a/common/pb/mgmt_pb/mgmt.pb.go
+++ b/common/pb/mgmt_pb/mgmt.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v3.21.12
+// 	protoc        v4.23.4
 // source: mgmt.proto
 
 package mgmt_pb
diff --git a/controller/db/identity_store.go b/controller/db/identity_store.go
index 991a57318..be12570f5 100644
--- a/controller/db/identity_store.go
+++ b/controller/db/identity_store.go
@@ -41,6 +41,8 @@ const (
 	FieldIdentityEnvInfoOs         = "envInfoOs"
 	FieldIdentityEnvInfoOsRelease  = "envInfoRelease"
 	FieldIdentityEnvInfoOsVersion  = "envInfoVersion"
+	FieldIdentityEnvInfoDomain     = "envInfoDomain"
+	FieldIdentityEnvInfoHostname   = "envInfoHostname"
 	FieldIdentitySdkInfoBranch     = "sdkInfoBranch"
 	FieldIdentitySdkInfoRevision   = "sdkInfoRevision"
 	FieldIdentitySdkInfoType       = "sdkInfoType"
@@ -75,6 +77,8 @@ type EnvInfo struct {
 	Os        string `json:"os"`
 	OsRelease string `json:"osRelease"`
 	OsVersion string `json:"osVersion"`
+	Domain    string `json:"domain"`
+	Hostname  string `json:"hostname"`
 }
 
 type SdkInfo struct {
@@ -256,6 +260,8 @@ func (store *identityStoreImpl) FillEntity(entity *Identity, bucket *boltz.Typed
 		Os:        bucket.GetStringWithDefault(FieldIdentityEnvInfoOs, ""),
 		OsRelease: bucket.GetStringWithDefault(FieldIdentityEnvInfoOsRelease, ""),
 		OsVersion: bucket.GetStringWithDefault(FieldIdentityEnvInfoOsVersion, ""),
+		Domain:    bucket.GetStringWithDefault(FieldIdentityEnvInfoDomain, ""),
+		Hostname:  bucket.GetStringWithDefault(FieldIdentityEnvInfoHostname, ""),
 	}
 
 	entity.ServiceHostingPrecedences = map[string]ziti.Precedence{}
@@ -306,6 +312,8 @@ func (store *identityStoreImpl) PersistEntity(entity *Identity, ctx *boltz.Persi
 		ctx.SetString(FieldIdentityEnvInfoOs, entity.EnvInfo.Os)
 		ctx.SetString(FieldIdentityEnvInfoOsRelease, entity.EnvInfo.OsRelease)
 		ctx.SetString(FieldIdentityEnvInfoOsVersion, entity.EnvInfo.OsVersion)
+		ctx.SetString(FieldIdentityEnvInfoDomain, entity.EnvInfo.Domain)
+		ctx.SetString(FieldIdentityEnvInfoHostname, entity.EnvInfo.Hostname)
 	}
 
 	if entity.SdkInfo != nil {
diff --git a/controller/handler_edge_ctrl/common_tunnel.go b/controller/handler_edge_ctrl/common_tunnel.go
index ab5e12f5a..d4bb4b0db 100644
--- a/controller/handler_edge_ctrl/common_tunnel.go
+++ b/controller/handler_edge_ctrl/common_tunnel.go
@@ -348,6 +348,8 @@ func (self *baseTunnelRequestContext) updateIdentityInfo(envInfo *edge_ctrl_pb.E
 				Os:        envInfo.Os,
 				OsRelease: envInfo.OsRelease,
 				OsVersion: envInfo.OsVersion,
+				Domain:    envInfo.Domain,
+				Hostname:  envInfo.Hostname,
 			}
 			if !self.identity.EnvInfo.Equals(newEnvInfo) {
 				self.identity.EnvInfo = newEnvInfo
diff --git a/controller/internal/routes/identity_api_model.go b/controller/internal/routes/identity_api_model.go
index 0f292e81e..dec979b0f 100644
--- a/controller/internal/routes/identity_api_model.go
+++ b/controller/internal/routes/identity_api_model.go
@@ -375,6 +375,8 @@ func fillInfo(identity *rest_model.IdentityDetail, envInfo *model.EnvInfo, sdkIn
 			Os:        envInfo.Os,
 			OsRelease: envInfo.OsRelease,
 			OsVersion: envInfo.OsVersion,
+			Domain:    envInfo.Domain,
+			Hostname:  envInfo.Hostname,
 		}
 	} else {
 		identity.EnvInfo = &rest_model.EnvInfo{}
diff --git a/controller/model/identity_manager.go b/controller/model/identity_manager.go
index c34f95022..40a5dc4ad 100644
--- a/controller/model/identity_manager.go
+++ b/controller/model/identity_manager.go
@@ -448,6 +448,8 @@ func (self *IdentityManager) PatchInfo(identity *Identity, changeCtx *change.Con
 		db.FieldIdentityEnvInfoOs:         struct{}{},
 		db.FieldIdentityEnvInfoOsRelease:  struct{}{},
 		db.FieldIdentityEnvInfoOsVersion:  struct{}{},
+		db.FieldIdentityEnvInfoDomain:     struct{}{},
+		db.FieldIdentityEnvInfoHostname:   struct{}{},
 		db.FieldIdentitySdkInfoBranch:     struct{}{},
 		db.FieldIdentitySdkInfoRevision:   struct{}{},
 		db.FieldIdentitySdkInfoType:       struct{}{},
@@ -571,6 +573,8 @@ func (self *IdentityManager) IdentityToProtobuf(entity *Identity) (*edge_cmd_pb.
 			Os:        entity.EnvInfo.Os,
 			OsRelease: entity.EnvInfo.OsRelease,
 			OsVersion: entity.EnvInfo.OsVersion,
+			Domain:    entity.EnvInfo.Domain,
+			Hostname:  entity.EnvInfo.Hostname,
 		}
 	}
 
@@ -642,6 +646,8 @@ func (self *IdentityManager) ProtobufToIdentity(msg *edge_cmd_pb.Identity) (*Ide
 			Os:        msg.EnvInfo.Os,
 			OsRelease: msg.EnvInfo.OsRelease,
 			OsVersion: msg.EnvInfo.OsVersion,
+			Domain:    msg.EnvInfo.Domain,
+			Hostname:  msg.EnvInfo.Hostname,
 		}
 	}
 
diff --git a/controller/model/identity_model.go b/controller/model/identity_model.go
index b9a54ac44..29c67adf8 100644
--- a/controller/model/identity_model.go
+++ b/controller/model/identity_model.go
@@ -31,6 +31,8 @@ type EnvInfo struct {
 	Os        string
 	OsRelease string
 	OsVersion string
+	Domain    string
+	Hostname  string
 }
 
 func (self *EnvInfo) Equals(other *EnvInfo) bool {
@@ -43,7 +45,9 @@ func (self *EnvInfo) Equals(other *EnvInfo) bool {
 	return self.Arch == other.Arch &&
 		self.Os == other.Os &&
 		self.OsRelease == other.OsRelease &&
-		self.OsVersion == other.OsVersion
+		self.OsVersion == other.OsVersion &&
+		self.Domain == other.Domain &&
+		self.Hostname == other.Hostname
 }
 
 type SdkInfo struct {
@@ -137,6 +141,8 @@ func (entity *Identity) toBoltEntityForCreate(_ *bbolt.Tx, env Env) (*db.Identit
 			Os:        entity.EnvInfo.Os,
 			OsRelease: entity.EnvInfo.OsRelease,
 			OsVersion: entity.EnvInfo.OsVersion,
+			Domain:    entity.EnvInfo.Domain,
+			Hostname:  entity.EnvInfo.Hostname,
 		}
 	}
 
@@ -162,6 +168,8 @@ func fillModelInfo(identity *Identity, envInfo *db.EnvInfo, sdkInfo *db.SdkInfo)
 			Os:        envInfo.Os,
 			OsRelease: envInfo.OsRelease,
 			OsVersion: envInfo.OsVersion,
+			Domain:    envInfo.Domain,
+			Hostname:  envInfo.Hostname,
 		}
 	}
 
@@ -184,6 +192,8 @@ func fillPersistenceInfo(identity *db.Identity, envInfo *EnvInfo, sdkInfo *SdkIn
 			Os:        envInfo.Os,
 			OsRelease: envInfo.OsRelease,
 			OsVersion: envInfo.OsVersion,
+			Domain:    envInfo.Domain,
+			Hostname:  envInfo.Hostname,
 		}
 	}
 
diff --git a/go.mod b/go.mod
index bfd88baa9..e1742866b 100644
--- a/go.mod
+++ b/go.mod
@@ -49,7 +49,7 @@ require (
 	github.com/natefinch/lumberjack v2.0.0+incompatible
 	github.com/openziti/agent v1.0.16
 	github.com/openziti/channel/v2 v2.0.119
-	github.com/openziti/edge-api v0.26.10
+	github.com/openziti/edge-api v0.26.11
 	github.com/openziti/foundation/v2 v2.0.37
 	github.com/openziti/identity v1.0.70
 	github.com/openziti/jwks v1.0.3
@@ -174,11 +174,11 @@ require (
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/yusufpapurcu/wmi v1.2.3 // indirect
-	go.mongodb.org/mongo-driver v1.13.1 // indirect
+	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
-	go.opentelemetry.io/otel v1.21.0 // indirect
-	go.opentelemetry.io/otel/metric v1.21.0 // indirect
-	go.opentelemetry.io/otel/trace v1.21.0 // indirect
+	go.opentelemetry.io/otel v1.23.1 // indirect
+	go.opentelemetry.io/otel/metric v1.23.1 // indirect
+	go.opentelemetry.io/otel/trace v1.23.1 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
 	golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
diff --git a/go.sum b/go.sum
index 3e5fb0829..cc2e357f8 100644
--- a/go.sum
+++ b/go.sum
@@ -283,7 +283,6 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
 github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
 github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4=
@@ -430,7 +429,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@@ -535,7 +533,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
 github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
 github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
@@ -571,8 +568,8 @@ github.com/openziti/channel/v2 v2.0.119 h1:stfSrnDqoTi78LMvQA3+NSivHjQnRrYKrgij5
 github.com/openziti/channel/v2 v2.0.119/go.mod h1:lSRJwqmbkE34DgXYEmUhVCzwcQcx65vZGE8nuBNK458=
 github.com/openziti/dilithium v0.3.3 h1:PLgQ6PMNLSTzCFbX/h98cmudgz/cU6TmjdSv5NAPD8k=
 github.com/openziti/dilithium v0.3.3/go.mod h1:vsCjI2AU/hon9e+dLhUFbCNGesJDj2ASgkySOcpmvjo=
-github.com/openziti/edge-api v0.26.10 h1:LEDuJHZsExi0PBVO9iVuIdZWJ7eFo/i4TJhXoSFmfOU=
-github.com/openziti/edge-api v0.26.10/go.mod h1:FQLjav9AfqxQYSL0xKPDZ/JWTSZXApkk7jM2/iczGXM=
+github.com/openziti/edge-api v0.26.11 h1:qINsfGpPBTnbuDrOq+qcMZuBdlXosqvHX7sQhLA+cM4=
+github.com/openziti/edge-api v0.26.11/go.mod h1:30SiUmR+9gOBi9HUZgXLpCO2nNCbNFVx2jwXV2Dh4Og=
 github.com/openziti/foundation/v2 v2.0.37 h1:7pa4vWrlwllEoLXaK2rx91AffLQJ8k5pvc92oWANavA=
 github.com/openziti/foundation/v2 v2.0.37/go.mod h1:2NxzCnJbMw35U9RrFcdEaiXdxIMfBHOUNPngpyhvKeY=
 github.com/openziti/identity v1.0.70 h1:JNwtJHmIS0DcXookm2xuXyh4z92T1O21GQvuO8PmHWs=
@@ -782,9 +779,6 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
 github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
 github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
-github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
-github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
-github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -792,7 +786,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
 github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -809,8 +802,8 @@ go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
-go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
-go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
+go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
+go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@@ -821,14 +814,14 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
-go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
-go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
-go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
+go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
+go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
+go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
 go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE=
 go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ=
-go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
-go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
+go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
@@ -863,7 +856,6 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
@@ -961,7 +953,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -1065,7 +1056,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1099,7 +1089,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
diff --git a/tests/auth_cert_test.go b/tests/auth_cert_test.go
index 853ff6208..4fde217d6 100644
--- a/tests/auth_cert_test.go
+++ b/tests/auth_cert_test.go
@@ -29,12 +29,12 @@ import (
 	"fmt"
 	"github.com/Jeffail/gabs"
 	"github.com/openziti/edge-api/rest_model"
+	nfPem "github.com/openziti/foundation/v2/pem"
 	"github.com/openziti/ziti/common/cert"
 	"github.com/openziti/ziti/common/eid"
+	"github.com/openziti/ziti/controller/change"
 	"github.com/openziti/ziti/controller/env"
 	"github.com/openziti/ziti/controller/model"
-	"github.com/openziti/ziti/controller/change"
-	nfPem "github.com/openziti/foundation/v2/pem"
 	"github.com/stretchr/testify/require"
 	"net/http"
 	"reflect"
@@ -190,7 +190,7 @@ func (test *authCertTests) testAuthenticateValidCertValidClientInfoBody(t *testi
 	transport.TLSClientConfig.Certificates = test.certAuthenticator.TLSCertificates()
 
 	bodyJson := `{
-  "envInfo": {"os": "windows", "arch": "amd64", "osRelease": "6.2.9200", "osVersion": "6.2.9200"},
+  "envInfo": {"os": "windows", "arch": "amd64", "osRelease": "6.2.9200", "osVersion": "6.2.9200", "domain": "domain1", "hostname": "hostname1"},
   "sdkInfo": {"type": "ziti-sdk-golang", "branch": "unknown", "version": "0.0.0", "revision": "unknown"}
 }`
 	resp, err := testClient.NewRequest().
@@ -287,7 +287,7 @@ func (test *authCertTests) testAuthenticateValidCertValidClientInfoBody(t *testi
 		r := test.ctx.Req
 
 		secondInfo := `{
-  "envInfo": {"os": "updatedValueOs", "arch": "updatedValueArch", "osRelease": "updatedValueRelease", "osVersion": "updatedValueOsRelease"},
+  "envInfo": {"os": "updatedValueOs", "arch": "updatedValueArch", "osRelease": "updatedValueRelease", "osVersion": "updatedValueOsRelease", "domain": "updatedDomain", "hostname": "updatedHostname"},
   "sdkInfo": {"type": "updatedValueType", "branch": "updatedValueBranch", "version": "updatedValueVersion", "revision": "updatedValueRevision"}
 }`
 		authResp, err := testClient.NewRequest().
diff --git a/zititest/go.mod b/zititest/go.mod
index d8a172c53..6245b9427 100644
--- a/zititest/go.mod
+++ b/zititest/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/michaelquigley/pfxlog v0.6.10
 	github.com/openziti/agent v1.0.16
 	github.com/openziti/channel/v2 v2.0.119
-	github.com/openziti/edge-api v0.26.10
+	github.com/openziti/edge-api v0.26.11
 	github.com/openziti/fablab v0.5.42
 	github.com/openziti/foundation/v2 v2.0.37
 	github.com/openziti/identity v1.0.70
@@ -177,11 +177,11 @@ require (
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
 	github.com/yusufpapurcu/wmi v1.2.3 // indirect
 	github.com/zitadel/oidc/v2 v2.12.0 // indirect
-	go.mongodb.org/mongo-driver v1.13.1 // indirect
+	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
-	go.opentelemetry.io/otel v1.21.0 // indirect
-	go.opentelemetry.io/otel/metric v1.21.0 // indirect
-	go.opentelemetry.io/otel/trace v1.21.0 // indirect
+	go.opentelemetry.io/otel v1.23.1 // indirect
+	go.opentelemetry.io/otel/metric v1.23.1 // indirect
+	go.opentelemetry.io/otel/trace v1.23.1 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
 	go4.org v0.0.0-20180809161055-417644f6feb5 // indirect
diff --git a/zititest/go.sum b/zititest/go.sum
index 2cdc64793..eb402271d 100644
--- a/zititest/go.sum
+++ b/zititest/go.sum
@@ -595,6 +595,7 @@ github.com/openziti/dilithium v0.3.3 h1:PLgQ6PMNLSTzCFbX/h98cmudgz/cU6TmjdSv5NAP
 github.com/openziti/dilithium v0.3.3/go.mod h1:vsCjI2AU/hon9e+dLhUFbCNGesJDj2ASgkySOcpmvjo=
 github.com/openziti/edge-api v0.26.10 h1:LEDuJHZsExi0PBVO9iVuIdZWJ7eFo/i4TJhXoSFmfOU=
 github.com/openziti/edge-api v0.26.10/go.mod h1:FQLjav9AfqxQYSL0xKPDZ/JWTSZXApkk7jM2/iczGXM=
+github.com/openziti/edge-api v0.26.11/go.mod h1:30SiUmR+9gOBi9HUZgXLpCO2nNCbNFVx2jwXV2Dh4Og=
 github.com/openziti/fablab v0.5.42 h1:vENJKfEba2T4sSLwlKDL/IzBYfY8iHnhc4umf6IESiY=
 github.com/openziti/fablab v0.5.42/go.mod h1:HDT06y1QX8kO8ZQrgHvZmJsvc8iRybESGtlDLDII4ks=
 github.com/openziti/foundation/v2 v2.0.37 h1:7pa4vWrlwllEoLXaK2rx91AffLQJ8k5pvc92oWANavA=
@@ -838,6 +839,7 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3
 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
 go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
 go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
+go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@@ -850,12 +852,15 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
 go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
 go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
 go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
 go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE=
 go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ=
 go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
 go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=

From 2708f4d804e8d4ec4a2396aae4846899b9a95b79 Mon Sep 17 00:00:00 2001
From: Andrew Martinez <andrew.p.martinez@gmail.com>
Date: Fri, 23 Feb 2024 13:30:47 -0500
Subject: [PATCH 25/46] go mod tidy for zititest

---
 zititest/go.sum | 26 +++++---------------------
 1 file changed, 5 insertions(+), 21 deletions(-)

diff --git a/zititest/go.sum b/zititest/go.sum
index eb402271d..dd53cdd95 100644
--- a/zititest/go.sum
+++ b/zititest/go.sum
@@ -288,7 +288,6 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
 github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
 github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4=
@@ -443,7 +442,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -551,7 +549,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
 github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
 github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
@@ -593,8 +590,7 @@ github.com/openziti/channel/v2 v2.0.119 h1:stfSrnDqoTi78LMvQA3+NSivHjQnRrYKrgij5
 github.com/openziti/channel/v2 v2.0.119/go.mod h1:lSRJwqmbkE34DgXYEmUhVCzwcQcx65vZGE8nuBNK458=
 github.com/openziti/dilithium v0.3.3 h1:PLgQ6PMNLSTzCFbX/h98cmudgz/cU6TmjdSv5NAPD8k=
 github.com/openziti/dilithium v0.3.3/go.mod h1:vsCjI2AU/hon9e+dLhUFbCNGesJDj2ASgkySOcpmvjo=
-github.com/openziti/edge-api v0.26.10 h1:LEDuJHZsExi0PBVO9iVuIdZWJ7eFo/i4TJhXoSFmfOU=
-github.com/openziti/edge-api v0.26.10/go.mod h1:FQLjav9AfqxQYSL0xKPDZ/JWTSZXApkk7jM2/iczGXM=
+github.com/openziti/edge-api v0.26.11 h1:qINsfGpPBTnbuDrOq+qcMZuBdlXosqvHX7sQhLA+cM4=
 github.com/openziti/edge-api v0.26.11/go.mod h1:30SiUmR+9gOBi9HUZgXLpCO2nNCbNFVx2jwXV2Dh4Og=
 github.com/openziti/fablab v0.5.42 h1:vENJKfEba2T4sSLwlKDL/IzBYfY8iHnhc4umf6IESiY=
 github.com/openziti/fablab v0.5.42/go.mod h1:HDT06y1QX8kO8ZQrgHvZmJsvc8iRybESGtlDLDII4ks=
@@ -810,9 +806,6 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
 github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
 github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
-github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
-github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
-github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@@ -820,7 +813,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
 github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -837,8 +829,7 @@ go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
-go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
-go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
+go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
 go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
@@ -850,16 +841,13 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
-go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
 go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
-go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
-go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
 go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
 go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE=
 go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ=
-go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
-go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
 go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -895,7 +883,6 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
@@ -993,7 +980,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -1097,7 +1083,6 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1131,7 +1116,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=

From 799dbef0e7012176f6ba94bdecfc10bf7939c0e6 Mon Sep 17 00:00:00 2001
From: Paul Lorenz <paul.lorenz@netfoundry.io>
Date: Tue, 27 Feb 2024 11:43:20 -0500
Subject: [PATCH 26/46] Fix create/update of tags from CLI. Fixes #1204

---
 ziti/cmd/api/options.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/ziti/cmd/api/options.go b/ziti/cmd/api/options.go
index d2d68b26a..ca6327a19 100644
--- a/ziti/cmd/api/options.go
+++ b/ziti/cmd/api/options.go
@@ -21,7 +21,7 @@ import (
 	"fmt"
 	"github.com/Jeffail/gabs"
 	"github.com/openziti/ziti/ziti/cmd/common"
-	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"io"
 )
@@ -89,9 +89,9 @@ func (self *EntityOptions) AddCommonFlags(cmd *cobra.Command) {
 
 func (self *EntityOptions) GetTags() map[string]interface{} {
 	result := map[string]interface{}{}
-	if len(self.Tags) > 0 {
+	if len(self.TagsJson) > 0 {
 		if err := json.Unmarshal([]byte(self.TagsJson), &result); err != nil {
-			panic(errors.Wrap(err, "invalid tags JSON"))
+			logrus.Fatalf("invalid tags JSON: '%s'", self.TagsJson)
 		}
 	}
 	for k, v := range self.Tags {

From 5aa6558061f7601795be2709383ab7112b3e5777 Mon Sep 17 00:00:00 2001
From: gberl002 <geoff.berl@netfoundry.io>
Date: Tue, 27 Feb 2024 13:23:37 -0500
Subject: [PATCH 27/46] Changed panic to lesser level, commented zac binding by
 default

Signed-off-by: gberl002 <geoff.berl@netfoundry.io>
---
 common/spa_handler/handler.go                   | 2 +-
 ziti/cmd/create/config_templates/controller.yml | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/common/spa_handler/handler.go b/common/spa_handler/handler.go
index 1f325269e..86a461242 100644
--- a/common/spa_handler/handler.go
+++ b/common/spa_handler/handler.go
@@ -49,7 +49,7 @@ func (factory ZitiAdminConsoleFactory) Binding() string {
 func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
 	loc := options["location"]
 	if loc == nil || loc == "" {
-		log.Panic("location must be supplied in zac options")
+		log.Fatal("location must be supplied in zac options")
 	}
 	indexFile := options["indexFile"]
 	if indexFile == nil || indexFile == "" {
diff --git a/ziti/cmd/create/config_templates/controller.yml b/ziti/cmd/create/config_templates/controller.yml
index 906a69657..1da02492b 100644
--- a/ziti/cmd/create/config_templates/controller.yml
+++ b/ziti/cmd/create/config_templates/controller.yml
@@ -215,5 +215,5 @@ web:
         options: { }
       - binding: fabric
         options: { }
-      - binding: zac
-        options: { }
+#      - binding: zac
+#        options: { }

From 261e12043cb2c28fd8f422fa8b500fc5a50a0d0f Mon Sep 17 00:00:00 2001
From: gberl002 <geoff.berl@netfoundry.io>
Date: Tue, 27 Feb 2024 14:34:02 -0500
Subject: [PATCH 28/46] Changing xweb to SPA for clarification

Signed-off-by: gberl002 <geoff.berl@netfoundry.io>
---
 controller/controller.go                     | 38 ++++++++++----------
 controller/env/appenv.go                     |  2 +-
 controller/internal/routes/version_router.go |  6 ++--
 controller/server/controller.go              |  6 ++--
 4 files changed, 26 insertions(+), 26 deletions(-)

diff --git a/controller/controller.go b/controller/controller.go
index d2cff2978..7476e4290 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -73,8 +73,8 @@ type Controller struct {
 	xctrls             []xctrl.Xctrl
 	xmgmts             []xmgmt.Xmgmt
 
-	xwebFactoryRegistry xweb.Registry
-	xweb                xweb.Instance
+	spaFactoryRegistry xweb.Registry
+	spaHandler         xweb.Instance
 
 	ctrlListener channel.UnderlayListener
 	mgmtListener channel.UnderlayListener
@@ -170,12 +170,12 @@ func NewController(cfg *Config, versionProvider versions.VersionProvider) (*Cont
 	log := pfxlog.Logger()
 
 	c := &Controller{
-		config:              cfg,
-		shutdownC:           shutdownC,
-		xwebFactoryRegistry: xweb.NewRegistryMap(),
-		metricsRegistry:     metricRegistry,
-		versionProvider:     versionProvider,
-		eventDispatcher:     events.NewDispatcher(shutdownC),
+		config:             cfg,
+		shutdownC:          shutdownC,
+		spaFactoryRegistry: xweb.NewRegistryMap(),
+		metricsRegistry:    metricRegistry,
+		versionProvider:    versionProvider,
+		eventDispatcher:    events.NewDispatcher(shutdownC),
 	}
 
 	if cfg.Raft != nil {
@@ -234,22 +234,22 @@ func (c *Controller) initWeb() {
 		logrus.WithError(err).Fatalf("failed to create health checker")
 	}
 
-	c.xweb = xweb.NewDefaultInstance(c.xwebFactoryRegistry, c.config.Id)
+	c.spaHandler = xweb.NewDefaultInstance(c.spaFactoryRegistry, c.config.Id)
 
-	if err := c.xweb.GetRegistry().Add(health.NewHealthCheckApiFactory(healthChecker)); err != nil {
+	if err := c.spaHandler.GetRegistry().Add(health.NewHealthCheckApiFactory(healthChecker)); err != nil {
 		logrus.WithError(err).Fatalf("failed to create health checks api factory")
 	}
 
-	if err := c.xweb.GetRegistry().Add(api_impl.NewManagementApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
+	if err := c.spaHandler.GetRegistry().Add(api_impl.NewManagementApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
 		logrus.WithError(err).Fatalf("failed to create management api factory")
 	}
 
-	if err := c.xweb.GetRegistry().Add(api_impl.NewMetricsApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
+	if err := c.spaHandler.GetRegistry().Add(api_impl.NewMetricsApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
 		logrus.WithError(err).Fatalf("failed to create metrics api factory")
 	}
 
-	if err := c.xweb.GetRegistry().Add(spa_handler.NewZitiAdminConsoleFactory()); err != nil {
-		logrus.WithError(err).Fatalf("failed to create myXweb factory")
+	if err := c.spaHandler.GetRegistry().Add(spa_handler.NewZitiAdminConsoleFactory()); err != nil {
+		logrus.WithError(err).Fatalf("failed to create single page application factory")
 	}
 
 }
@@ -313,11 +313,11 @@ func (c *Controller) Run() error {
 
 	go underlayDispatcher.Run()
 
-	if err := c.config.Configure(c.xweb); err != nil {
+	if err := c.config.Configure(c.spaHandler); err != nil {
 		panic(err)
 	}
 
-	go c.xweb.Run()
+	go c.spaHandler.Run()
 
 	// event handlers
 	if err := c.eventDispatcher.WireEventHandlers(c.getEventHandlerConfigs()); err != nil {
@@ -373,7 +373,7 @@ func (c *Controller) Shutdown() {
 			}
 		}
 
-		go c.xweb.Shutdown()
+		go c.spaHandler.Shutdown()
 	}
 }
 
@@ -436,8 +436,8 @@ func (c *Controller) RegisterXmgmt(x xmgmt.Xmgmt) error {
 	return nil
 }
 
-func (c *Controller) GetXWebInstance() xweb.Instance {
-	return c.xweb
+func (c *Controller) GetSPAInstance() xweb.Instance {
+	return c.spaHandler
 }
 
 func (c *Controller) GetNetwork() *network.Network {
diff --git a/controller/env/appenv.go b/controller/env/appenv.go
index c4e5bd314..10cc9ba3f 100644
--- a/controller/env/appenv.go
+++ b/controller/env/appenv.go
@@ -201,7 +201,7 @@ type HostController interface {
 	RegisterAgentBindHandler(bindHandler channel.BindHandler)
 	RegisterXctrl(x xctrl.Xctrl) error
 	RegisterXmgmt(x xmgmt.Xmgmt) error
-	GetXWebInstance() xweb.Instance
+	GetSPAInstance() xweb.Instance
 	GetNetwork() *network.Network
 	GetCloseNotifyChannel() <-chan struct{}
 	Shutdown()
diff --git a/controller/internal/routes/version_router.go b/controller/internal/routes/version_router.go
index be613c91f..bf82e66c3 100644
--- a/controller/internal/routes/version_router.go
+++ b/controller/internal/routes/version_router.go
@@ -22,12 +22,12 @@ import (
 	clientInformational "github.com/openziti/edge-api/rest_client_api_server/operations/informational"
 	managementInformational "github.com/openziti/edge-api/rest_management_api_server/operations/informational"
 	"github.com/openziti/edge-api/rest_model"
+	"github.com/openziti/xweb/v2"
+	"github.com/openziti/ziti/common/build"
 	"github.com/openziti/ziti/controller"
 	"github.com/openziti/ziti/controller/env"
 	"github.com/openziti/ziti/controller/internal/permissions"
 	"github.com/openziti/ziti/controller/response"
-	"github.com/openziti/ziti/common/build"
-	"github.com/openziti/xweb/v2"
 	"runtime"
 	"sync"
 )
@@ -118,7 +118,7 @@ func (ir *VersionRouter) List(ae *env.AppEnv, rc *response.RequestContext) {
 
 		oidcEnabled := false
 
-		for _, serverConfig := range ae.HostController.GetXWebInstance().GetConfig().ServerConfigs {
+		for _, serverConfig := range ae.HostController.GetSPAInstance().GetConfig().ServerConfigs {
 			for _, api := range serverConfig.APIs {
 				if api.Binding() == controller.OidcApiBinding {
 					oidcEnabled = true
diff --git a/controller/server/controller.go b/controller/server/controller.go
index 579b86eb4..4ce5f944f 100644
--- a/controller/server/controller.go
+++ b/controller/server/controller.go
@@ -291,15 +291,15 @@ func (c *Controller) Run() {
 	clientApiFactory := NewClientApiFactory(c.AppEnv)
 	oidcApiFactory := NewOidcApiFactory(c.AppEnv)
 
-	if err := c.AppEnv.HostController.GetXWebInstance().GetRegistry().Add(managementApiFactory); err != nil {
+	if err := c.AppEnv.HostController.GetSPAInstance().GetRegistry().Add(managementApiFactory); err != nil {
 		pfxlog.Logger().Fatalf("failed to create Edge Management API factory: %v", err)
 	}
 
-	if err := c.AppEnv.HostController.GetXWebInstance().GetRegistry().Add(clientApiFactory); err != nil {
+	if err := c.AppEnv.HostController.GetSPAInstance().GetRegistry().Add(clientApiFactory); err != nil {
 		pfxlog.Logger().Fatalf("failed to create Edge Client API factory: %v", err)
 	}
 
-	if err := c.AppEnv.HostController.GetXWebInstance().GetRegistry().Add(oidcApiFactory); err != nil {
+	if err := c.AppEnv.HostController.GetSPAInstance().GetRegistry().Add(oidcApiFactory); err != nil {
 		pfxlog.Logger().Fatalf("failed to create OIDC API factory: %v", err)
 	}
 

From 5ebaf90f6b0f132482c159191e1c6897a212ce8d Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Tue, 27 Feb 2024 18:17:19 -0500
Subject: [PATCH 29/46] update template and rename zac to spa

---
 common/spa_handler/handler.go                 | 37 +++++++++----------
 .../create/config_templates/controller.yml    |  2 +-
 2 files changed, 19 insertions(+), 20 deletions(-)

diff --git a/common/spa_handler/handler.go b/common/spa_handler/handler.go
index 86a461242..2b9659726 100644
--- a/common/spa_handler/handler.go
+++ b/common/spa_handler/handler.go
@@ -26,63 +26,64 @@ import (
 )
 
 const (
-	Binding = "zac"
+	Binding = "spa"
 )
 
-type ZitiAdminConsoleFactory struct {
+type SinglePageAppFactory struct {
 }
 
-var _ xweb.ApiHandlerFactory = &ZitiAdminConsoleFactory{}
+var _ xweb.ApiHandlerFactory = &SinglePageAppFactory{}
 
-func NewZitiAdminConsoleFactory() *ZitiAdminConsoleFactory {
-	return &ZitiAdminConsoleFactory{}
+func NewSinglePageAppFactory() *SinglePageAppFactory {
+	return &SinglePageAppFactory{}
 }
 
-func (factory ZitiAdminConsoleFactory) Validate(*xweb.InstanceConfig) error {
+func (factory SinglePageAppFactory) Validate(*xweb.InstanceConfig) error {
 	return nil
 }
 
-func (factory ZitiAdminConsoleFactory) Binding() string {
+func (factory SinglePageAppFactory) Binding() string {
 	return Binding
 }
 
-func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
+func (factory SinglePageAppFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
 	loc := options["location"]
 	if loc == nil || loc == "" {
-		log.Fatal("location must be supplied in zac options")
+		log.Panic("location must be supplied in spa options")
 	}
 	indexFile := options["indexFile"]
 	if indexFile == nil || indexFile == "" {
 		indexFile = "index.html"
 	}
-	zac := &SPAHTTPHandler{
+	spa := &SinglePageAppHandler{
 		httpHandler: SpaHandler(loc.(string), "/"+Binding, indexFile.(string)),
 	}
 
-	return zac, nil
+	log.Infof("intializing SPA Handler from %s", loc)
+	return spa, nil
 }
 
-type SPAHTTPHandler struct {
+type SinglePageAppHandler struct {
 	httpHandler http.Handler
 }
 
-func (self *SPAHTTPHandler) Binding() string {
+func (self *SinglePageAppHandler) Binding() string {
 	return Binding
 }
 
-func (self *SPAHTTPHandler) Options() map[interface{}]interface{} {
+func (self *SinglePageAppHandler) Options() map[interface{}]interface{} {
 	return nil
 }
 
-func (self *SPAHTTPHandler) RootPath() string {
+func (self *SinglePageAppHandler) RootPath() string {
 	return "/" + Binding
 }
 
-func (self *SPAHTTPHandler) IsHandler(r *http.Request) bool {
+func (self *SinglePageAppHandler) IsHandler(r *http.Request) bool {
 	return strings.HasPrefix(r.URL.Path, self.RootPath()) || strings.HasPrefix(r.URL.Path, "/assets")
 }
 
-func (self *SPAHTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+func (self *SinglePageAppHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
 	self.httpHandler.ServeHTTP(writer, request)
 }
 
@@ -99,10 +100,8 @@ type spaHandler struct {
 // (2) Request path is a directory
 // Otherwise serves the requested file.
 func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	log.Debugf("incoming r.URL.Path: %s", r.URL.Path)
 	r.URL.Path = strings.TrimPrefix(r.URL.Path, h.contextRoot)
 	p := filepath.Join(h.content, filepath.Clean(r.URL.Path))
-	log.Debugf("outgoing r.URL.Path: %s", p)
 
 	if info, err := os.Stat(p); err != nil {
 		http.ServeFile(w, r, filepath.Join(h.content, h.indexFile))
diff --git a/ziti/cmd/create/config_templates/controller.yml b/ziti/cmd/create/config_templates/controller.yml
index 1da02492b..d476983e9 100644
--- a/ziti/cmd/create/config_templates/controller.yml
+++ b/ziti/cmd/create/config_templates/controller.yml
@@ -216,4 +216,4 @@ web:
       - binding: fabric
         options: { }
 #      - binding: zac
-#        options: { }
+#        options: { "location": "./zac", "indexFile":"index.html" }

From 26698531c46cb864f29dc3459398bd114067f7b0 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Tue, 27 Feb 2024 18:18:59 -0500
Subject: [PATCH 30/46] rename from NewZitiAdminConsoleFactory to
 NewSinglePageAppFactory

---
 controller/controller.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/controller/controller.go b/controller/controller.go
index 7476e4290..a600aecfb 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -248,7 +248,7 @@ func (c *Controller) initWeb() {
 		logrus.WithError(err).Fatalf("failed to create metrics api factory")
 	}
 
-	if err := c.spaHandler.GetRegistry().Add(spa_handler.NewZitiAdminConsoleFactory()); err != nil {
+	if err := c.spaHandler.GetRegistry().Add(spa_handler.NewSinglePageAppFactory()); err != nil {
 		logrus.WithError(err).Fatalf("failed to create single page application factory")
 	}
 

From 1f94fd5ebe5e95e01cf74a697090e35135f0ffa2 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Tue, 27 Feb 2024 18:31:33 -0500
Subject: [PATCH 31/46] fix refactor gone awry

---
 controller/controller.go                     | 2 +-
 controller/env/appenv.go                     | 2 +-
 controller/internal/routes/version_router.go | 2 +-
 controller/server/controller.go              | 6 +++---
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/controller/controller.go b/controller/controller.go
index a600aecfb..c8d129227 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -436,7 +436,7 @@ func (c *Controller) RegisterXmgmt(x xmgmt.Xmgmt) error {
 	return nil
 }
 
-func (c *Controller) GetSPAInstance() xweb.Instance {
+func (c *Controller) GetXWebInstance() xweb.Instance {
 	return c.spaHandler
 }
 
diff --git a/controller/env/appenv.go b/controller/env/appenv.go
index 10cc9ba3f..c4e5bd314 100644
--- a/controller/env/appenv.go
+++ b/controller/env/appenv.go
@@ -201,7 +201,7 @@ type HostController interface {
 	RegisterAgentBindHandler(bindHandler channel.BindHandler)
 	RegisterXctrl(x xctrl.Xctrl) error
 	RegisterXmgmt(x xmgmt.Xmgmt) error
-	GetSPAInstance() xweb.Instance
+	GetXWebInstance() xweb.Instance
 	GetNetwork() *network.Network
 	GetCloseNotifyChannel() <-chan struct{}
 	Shutdown()
diff --git a/controller/internal/routes/version_router.go b/controller/internal/routes/version_router.go
index bf82e66c3..9b2663d3c 100644
--- a/controller/internal/routes/version_router.go
+++ b/controller/internal/routes/version_router.go
@@ -118,7 +118,7 @@ func (ir *VersionRouter) List(ae *env.AppEnv, rc *response.RequestContext) {
 
 		oidcEnabled := false
 
-		for _, serverConfig := range ae.HostController.GetSPAInstance().GetConfig().ServerConfigs {
+		for _, serverConfig := range ae.HostController.GetXWebInstance().GetConfig().ServerConfigs {
 			for _, api := range serverConfig.APIs {
 				if api.Binding() == controller.OidcApiBinding {
 					oidcEnabled = true
diff --git a/controller/server/controller.go b/controller/server/controller.go
index 4ce5f944f..579b86eb4 100644
--- a/controller/server/controller.go
+++ b/controller/server/controller.go
@@ -291,15 +291,15 @@ func (c *Controller) Run() {
 	clientApiFactory := NewClientApiFactory(c.AppEnv)
 	oidcApiFactory := NewOidcApiFactory(c.AppEnv)
 
-	if err := c.AppEnv.HostController.GetSPAInstance().GetRegistry().Add(managementApiFactory); err != nil {
+	if err := c.AppEnv.HostController.GetXWebInstance().GetRegistry().Add(managementApiFactory); err != nil {
 		pfxlog.Logger().Fatalf("failed to create Edge Management API factory: %v", err)
 	}
 
-	if err := c.AppEnv.HostController.GetSPAInstance().GetRegistry().Add(clientApiFactory); err != nil {
+	if err := c.AppEnv.HostController.GetXWebInstance().GetRegistry().Add(clientApiFactory); err != nil {
 		pfxlog.Logger().Fatalf("failed to create Edge Client API factory: %v", err)
 	}
 
-	if err := c.AppEnv.HostController.GetSPAInstance().GetRegistry().Add(oidcApiFactory); err != nil {
+	if err := c.AppEnv.HostController.GetXWebInstance().GetRegistry().Add(oidcApiFactory); err != nil {
 		pfxlog.Logger().Fatalf("failed to create OIDC API factory: %v", err)
 	}
 

From 5b2d67e80bae94ec5cc65ddd0c0b67bfa89d1c29 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Tue, 27 Feb 2024 20:24:39 -0500
Subject: [PATCH 32/46] rearrange where the zac handler goes and keep spa
 handler separate

---
 common/spa_handler/handler.go | 51 +++++------------------------------
 controller/controller.go      |  4 +--
 controller/zac/factory.go     | 46 +++++++++++++++++++++++++++++++
 3 files changed, 54 insertions(+), 47 deletions(-)
 create mode 100644 controller/zac/factory.go

diff --git a/common/spa_handler/handler.go b/common/spa_handler/handler.go
index 2b9659726..95150de16 100644
--- a/common/spa_handler/handler.go
+++ b/common/spa_handler/handler.go
@@ -17,58 +17,19 @@
 package spa_handler
 
 import (
-	"github.com/openziti/xweb/v2"
-	log "github.com/sirupsen/logrus"
 	"net/http"
 	"os"
 	"path/filepath"
 	"strings"
 )
 
-const (
-	Binding = "spa"
-)
-
-type SinglePageAppFactory struct {
-}
-
-var _ xweb.ApiHandlerFactory = &SinglePageAppFactory{}
-
-func NewSinglePageAppFactory() *SinglePageAppFactory {
-	return &SinglePageAppFactory{}
-}
-
-func (factory SinglePageAppFactory) Validate(*xweb.InstanceConfig) error {
-	return nil
-}
-
-func (factory SinglePageAppFactory) Binding() string {
-	return Binding
-}
-
-func (factory SinglePageAppFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
-	loc := options["location"]
-	if loc == nil || loc == "" {
-		log.Panic("location must be supplied in spa options")
-	}
-	indexFile := options["indexFile"]
-	if indexFile == nil || indexFile == "" {
-		indexFile = "index.html"
-	}
-	spa := &SinglePageAppHandler{
-		httpHandler: SpaHandler(loc.(string), "/"+Binding, indexFile.(string)),
-	}
-
-	log.Infof("intializing SPA Handler from %s", loc)
-	return spa, nil
-}
-
 type SinglePageAppHandler struct {
-	httpHandler http.Handler
+	HttpHandler http.Handler
+	BindingKey  string
 }
 
 func (self *SinglePageAppHandler) Binding() string {
-	return Binding
+	return self.BindingKey
 }
 
 func (self *SinglePageAppHandler) Options() map[interface{}]interface{} {
@@ -76,7 +37,7 @@ func (self *SinglePageAppHandler) Options() map[interface{}]interface{} {
 }
 
 func (self *SinglePageAppHandler) RootPath() string {
-	return "/" + Binding
+	return "/" + self.BindingKey
 }
 
 func (self *SinglePageAppHandler) IsHandler(r *http.Request) bool {
@@ -84,7 +45,7 @@ func (self *SinglePageAppHandler) IsHandler(r *http.Request) bool {
 }
 
 func (self *SinglePageAppHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
-	self.httpHandler.ServeHTTP(writer, request)
+	self.HttpHandler.ServeHTTP(writer, request)
 }
 
 // Thanks to https://github.com/roberthodgen/spa-server
@@ -114,7 +75,7 @@ func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	http.ServeFile(w, r, p)
 }
 
-// Returns a request handler (http.Handler) that serves a single
+// SpaHandler returns a request handler (http.Handler) that serves a single
 // page application from a given public directory (location).
 func SpaHandler(location string, contextRoot string, indexFile string) http.Handler {
 	return &spaHandler{location, contextRoot, indexFile}
diff --git a/controller/controller.go b/controller/controller.go
index c8d129227..4656d5572 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -25,10 +25,10 @@ import (
 	"github.com/openziti/transport/v2"
 	"github.com/openziti/ziti/common/capabilities"
 	"github.com/openziti/ziti/common/config"
-	"github.com/openziti/ziti/common/spa_handler"
 	"github.com/openziti/ziti/controller/event"
 	"github.com/openziti/ziti/controller/events"
 	"github.com/openziti/ziti/controller/handler_peer_ctrl"
+	"github.com/openziti/ziti/controller/zac"
 	"math/big"
 	"os"
 	"sync/atomic"
@@ -248,7 +248,7 @@ func (c *Controller) initWeb() {
 		logrus.WithError(err).Fatalf("failed to create metrics api factory")
 	}
 
-	if err := c.spaHandler.GetRegistry().Add(spa_handler.NewSinglePageAppFactory()); err != nil {
+	if err := c.spaHandler.GetRegistry().Add(zac.NewZitiAdminConsoleFactory()); err != nil {
 		logrus.WithError(err).Fatalf("failed to create single page application factory")
 	}
 
diff --git a/controller/zac/factory.go b/controller/zac/factory.go
new file mode 100644
index 000000000..ebd58e32f
--- /dev/null
+++ b/controller/zac/factory.go
@@ -0,0 +1,46 @@
+package zac
+
+import (
+	"github.com/openziti/xweb/v2"
+	"github.com/openziti/ziti/common/spa_handler"
+	log "github.com/sirupsen/logrus"
+)
+
+const (
+	Binding = "zac"
+)
+
+type ZitiAdminConsoleFactory struct {
+}
+
+var _ xweb.ApiHandlerFactory = &ZitiAdminConsoleFactory{}
+
+func NewZitiAdminConsoleFactory() *ZitiAdminConsoleFactory {
+	return &ZitiAdminConsoleFactory{}
+}
+
+func (factory ZitiAdminConsoleFactory) Validate(*xweb.InstanceConfig) error {
+	return nil
+}
+
+func (factory ZitiAdminConsoleFactory) Binding() string {
+	return Binding
+}
+
+func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
+	loc := options["location"]
+	if loc == nil || loc == "" {
+		log.Panic("location must be supplied in spa options")
+	}
+	indexFile := options["indexFile"]
+	if indexFile == nil || indexFile == "" {
+		indexFile = "index.html"
+	}
+	spa := &spa_handler.SinglePageAppHandler{
+		HttpHandler: spa_handler.SpaHandler(loc.(string), "/"+Binding, indexFile.(string)),
+		BindingKey:  Binding,
+	}
+
+	log.Infof("intializing ZAC SPA Handler from %s", loc)
+	return spa, nil
+}

From e8080bdd1bb4dfceb339f1236a7f84755496ecae Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Tue, 27 Feb 2024 20:26:16 -0500
Subject: [PATCH 33/46] minor refactor

---
 common/spa_handler/handler.go | 18 +++++++++---------
 controller/zac/factory.go     |  2 +-
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/common/spa_handler/handler.go b/common/spa_handler/handler.go
index 95150de16..11d310eba 100644
--- a/common/spa_handler/handler.go
+++ b/common/spa_handler/handler.go
@@ -28,24 +28,24 @@ type SinglePageAppHandler struct {
 	BindingKey  string
 }
 
-func (self *SinglePageAppHandler) Binding() string {
-	return self.BindingKey
+func (spa *SinglePageAppHandler) Binding() string {
+	return spa.BindingKey
 }
 
-func (self *SinglePageAppHandler) Options() map[interface{}]interface{} {
+func (spa *SinglePageAppHandler) Options() map[interface{}]interface{} {
 	return nil
 }
 
-func (self *SinglePageAppHandler) RootPath() string {
-	return "/" + self.BindingKey
+func (spa *SinglePageAppHandler) RootPath() string {
+	return "/" + spa.BindingKey
 }
 
-func (self *SinglePageAppHandler) IsHandler(r *http.Request) bool {
-	return strings.HasPrefix(r.URL.Path, self.RootPath()) || strings.HasPrefix(r.URL.Path, "/assets")
+func (spa *SinglePageAppHandler) IsHandler(r *http.Request) bool {
+	return strings.HasPrefix(r.URL.Path, spa.RootPath()) || strings.HasPrefix(r.URL.Path, "/assets")
 }
 
-func (self *SinglePageAppHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
-	self.HttpHandler.ServeHTTP(writer, request)
+func (spa *SinglePageAppHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	spa.HttpHandler.ServeHTTP(writer, request)
 }
 
 // Thanks to https://github.com/roberthodgen/spa-server
diff --git a/controller/zac/factory.go b/controller/zac/factory.go
index ebd58e32f..6f814b491 100644
--- a/controller/zac/factory.go
+++ b/controller/zac/factory.go
@@ -30,7 +30,7 @@ func (factory ZitiAdminConsoleFactory) Binding() string {
 func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) {
 	loc := options["location"]
 	if loc == nil || loc == "" {
-		log.Panic("location must be supplied in spa options")
+		log.Fatal("location must be supplied in " + Binding + " options")
 	}
 	indexFile := options["indexFile"]
 	if indexFile == nil || indexFile == "" {

From a7a6c82a0a06aa3f856d43c855e432f1bc61e142 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Tue, 27 Feb 2024 20:28:13 -0500
Subject: [PATCH 34/46] initializing

---
 controller/zac/factory.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/controller/zac/factory.go b/controller/zac/factory.go
index 6f814b491..be2721433 100644
--- a/controller/zac/factory.go
+++ b/controller/zac/factory.go
@@ -41,6 +41,6 @@ func (factory ZitiAdminConsoleFactory) New(_ *xweb.ServerConfig, options map[int
 		BindingKey:  Binding,
 	}
 
-	log.Infof("intializing ZAC SPA Handler from %s", loc)
+	log.Infof("initializing ZAC SPA Handler from %s", loc)
 	return spa, nil
 }

From 8d2b239db96039e611fdb4c45fd05c03a6345ab7 Mon Sep 17 00:00:00 2001
From: dovholuknf <46322585+dovholuknf@users.noreply.github.com>
Date: Tue, 27 Feb 2024 20:43:58 -0500
Subject: [PATCH 35/46] actually undo unexpected refactor

---
 controller/controller.go | 34 +++++++++++++++++-----------------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/controller/controller.go b/controller/controller.go
index 4656d5572..aefec258a 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -73,8 +73,8 @@ type Controller struct {
 	xctrls             []xctrl.Xctrl
 	xmgmts             []xmgmt.Xmgmt
 
-	spaFactoryRegistry xweb.Registry
-	spaHandler         xweb.Instance
+	xwebFactoryRegistry xweb.Registry
+	xweb                xweb.Instance
 
 	ctrlListener channel.UnderlayListener
 	mgmtListener channel.UnderlayListener
@@ -170,12 +170,12 @@ func NewController(cfg *Config, versionProvider versions.VersionProvider) (*Cont
 	log := pfxlog.Logger()
 
 	c := &Controller{
-		config:             cfg,
-		shutdownC:          shutdownC,
-		spaFactoryRegistry: xweb.NewRegistryMap(),
-		metricsRegistry:    metricRegistry,
-		versionProvider:    versionProvider,
-		eventDispatcher:    events.NewDispatcher(shutdownC),
+		config:              cfg,
+		shutdownC:           shutdownC,
+		xwebFactoryRegistry: xweb.NewRegistryMap(),
+		metricsRegistry:     metricRegistry,
+		versionProvider:     versionProvider,
+		eventDispatcher:     events.NewDispatcher(shutdownC),
 	}
 
 	if cfg.Raft != nil {
@@ -234,21 +234,21 @@ func (c *Controller) initWeb() {
 		logrus.WithError(err).Fatalf("failed to create health checker")
 	}
 
-	c.spaHandler = xweb.NewDefaultInstance(c.spaFactoryRegistry, c.config.Id)
+	c.xweb = xweb.NewDefaultInstance(c.xwebFactoryRegistry, c.config.Id)
 
-	if err := c.spaHandler.GetRegistry().Add(health.NewHealthCheckApiFactory(healthChecker)); err != nil {
+	if err := c.xweb.GetRegistry().Add(health.NewHealthCheckApiFactory(healthChecker)); err != nil {
 		logrus.WithError(err).Fatalf("failed to create health checks api factory")
 	}
 
-	if err := c.spaHandler.GetRegistry().Add(api_impl.NewManagementApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
+	if err := c.xweb.GetRegistry().Add(api_impl.NewManagementApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
 		logrus.WithError(err).Fatalf("failed to create management api factory")
 	}
 
-	if err := c.spaHandler.GetRegistry().Add(api_impl.NewMetricsApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
+	if err := c.xweb.GetRegistry().Add(api_impl.NewMetricsApiFactory(c.config.Id, c.network, c.xmgmts)); err != nil {
 		logrus.WithError(err).Fatalf("failed to create metrics api factory")
 	}
 
-	if err := c.spaHandler.GetRegistry().Add(zac.NewZitiAdminConsoleFactory()); err != nil {
+	if err := c.xweb.GetRegistry().Add(zac.NewZitiAdminConsoleFactory()); err != nil {
 		logrus.WithError(err).Fatalf("failed to create single page application factory")
 	}
 
@@ -313,11 +313,11 @@ func (c *Controller) Run() error {
 
 	go underlayDispatcher.Run()
 
-	if err := c.config.Configure(c.spaHandler); err != nil {
+	if err := c.config.Configure(c.xweb); err != nil {
 		panic(err)
 	}
 
-	go c.spaHandler.Run()
+	go c.xweb.Run()
 
 	// event handlers
 	if err := c.eventDispatcher.WireEventHandlers(c.getEventHandlerConfigs()); err != nil {
@@ -373,7 +373,7 @@ func (c *Controller) Shutdown() {
 			}
 		}
 
-		go c.spaHandler.Shutdown()
+		go c.xweb.Shutdown()
 	}
 }
 
@@ -437,7 +437,7 @@ func (c *Controller) RegisterXmgmt(x xmgmt.Xmgmt) error {
 }
 
 func (c *Controller) GetXWebInstance() xweb.Instance {
-	return c.spaHandler
+	return c.xweb
 }
 
 func (c *Controller) GetNetwork() *network.Network {

From e5ed982c26c6ebf0ab2178fa3cbbb058cfc29ce4 Mon Sep 17 00:00:00 2001
From: Shawn Carey <shawn.carey@netfoundry.io>
Date: Wed, 28 Feb 2024 03:51:29 +0000
Subject: [PATCH 36/46] just log error when dns server self-test fails (#1767)

---
 tunnel/dns/server.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/tunnel/dns/server.go b/tunnel/dns/server.go
index c6652a77f..07c782834 100644
--- a/tunnel/dns/server.go
+++ b/tunnel/dns/server.go
@@ -127,8 +127,7 @@ func NewDnsServer(addr string) (Resolver, error) {
 
 	err := r.testSystemResolver()
 	if err != nil {
-		_ = r.Cleanup()
-		return nil, fmt.Errorf("system resolver test failed: %s\n\n"+resolverConfigHelp, err, addr)
+		log.Errorf("system resolver test failed: %s\n\n"+resolverConfigHelp, err, addr)
 	}
 
 	return r, nil

From 8482268d4353b16a2596d61c965aea84361d7d27 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Wed, 28 Feb 2024 14:47:23 -0500
Subject: [PATCH 37/46] exclude instances named like 'flow-control.*'

---
 zititest/scripts/housekeeper-aws.bash | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/zititest/scripts/housekeeper-aws.bash b/zititest/scripts/housekeeper-aws.bash
index 40449b315..f03f3d737 100644
--- a/zititest/scripts/housekeeper-aws.bash
+++ b/zititest/scripts/housekeeper-aws.bash
@@ -18,9 +18,9 @@ function describe_instances() {
         --arg region "$region" \
         --arg oldest "$oldest" '
         [
-          .[]
-          |.[]
+          .[][]
           |select(.LaunchTime < $oldest)
+          | select(.Tags[] | select(.Key=="Name").Value | test("flow-control\\.*") | not )
           |{InstanceId: .InstanceId, Region: $region, LaunchTime: .LaunchTime, State: .State, Tags: .Tags}
         ]
       ' \

From 37b2fe5a0ee44a244fce6aa75688dac1779a59c9 Mon Sep 17 00:00:00 2001
From: Paul Lorenz <paul.lorenz@netfoundry.io>
Date: Fri, 1 Mar 2024 14:06:36 -0500
Subject: [PATCH 38/46] Fix check for timeout error. Fixes #1791

---
 router/handler_ctrl/route.go              |  8 +++++-
 router/handler_ctrl/timeout_check_test.go | 30 +++++++++++++++++++++++
 2 files changed, 37 insertions(+), 1 deletion(-)
 create mode 100644 router/handler_ctrl/timeout_check_test.go

diff --git a/router/handler_ctrl/route.go b/router/handler_ctrl/route.go
index ce43feee3..59061268f 100644
--- a/router/handler_ctrl/route.go
+++ b/router/handler_ctrl/route.go
@@ -18,6 +18,7 @@ package handler_ctrl
 
 import (
 	"github.com/openziti/ziti/router/env"
+	"net"
 	"syscall"
 	"time"
 
@@ -178,7 +179,7 @@ func (rh *routeHandler) connectEgress(msg *channel.Message, attempt int, ch chan
 				switch {
 				case errors.Is(err, syscall.ECONNREFUSED):
 					errCode = ctrl_msg.ErrorTypeConnectionRefused
-				case errors.Is(err, syscall.ETIMEDOUT):
+				case isNetworkTimeout(err) || errors.Is(err, syscall.ETIMEDOUT):
 					errCode = ctrl_msg.ErrorTypeDialTimedOut
 				case errors.As(err, &xgress.MisconfiguredTerminatorError{}):
 					errCode = ctrl_msg.ErrorTypeMisconfiguredTerminator
@@ -200,6 +201,11 @@ func (rh *routeHandler) connectEgress(msg *channel.Message, attempt int, ch chan
 	}
 }
 
+func isNetworkTimeout(err error) bool {
+	var netErr net.Error
+	return errors.As(err, &netErr)
+}
+
 func newDialParams(ctrlId string, route *ctrl_pb.Route, bindHandler xgress.BindHandler, logContext logcontext.Context, deadline time.Time) *dialParams {
 	return &dialParams{
 		ctrlId:      ctrlId,
diff --git a/router/handler_ctrl/timeout_check_test.go b/router/handler_ctrl/timeout_check_test.go
new file mode 100644
index 000000000..dd1b9d9f1
--- /dev/null
+++ b/router/handler_ctrl/timeout_check_test.go
@@ -0,0 +1,30 @@
+package handler_ctrl
+
+import (
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+type testTimeoutErr struct{}
+
+func (t testTimeoutErr) Error() string {
+	return "test"
+}
+
+func (t testTimeoutErr) Timeout() bool {
+	return true
+}
+
+func (t testTimeoutErr) Temporary() bool {
+	return true
+}
+
+func Test_TimeoutCheck(t *testing.T) {
+	err := testTimeoutErr{}
+	req := assert.New(t)
+	req.True(isNetworkTimeout(err))
+
+	wrapped := fmt.Errorf("there was an error (%w)", err)
+	req.True(isNetworkTimeout(wrapped))
+}

From 4b327c2000feef3b9853af4cfb738d0fac987081 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 5 Mar 2024 10:29:40 -0500
Subject: [PATCH 39/46] also expose the ctrl endpoint in nginx ingress example

---
 quickstart/kubernetes/miniziti.bash | 1 +
 1 file changed, 1 insertion(+)

diff --git a/quickstart/kubernetes/miniziti.bash b/quickstart/kubernetes/miniziti.bash
index b7db23a32..99d76df51 100644
--- a/quickstart/kubernetes/miniziti.bash
+++ b/quickstart/kubernetes/miniziti.bash
@@ -795,6 +795,7 @@ main(){
     }
     helmWrapper upgrade --install "ziti-controller" "${ZITI_CHARTS_REF}/ziti-controller" \
         --namespace "${ZITI_NAMESPACE}" --create-namespace \
+        --set ctrlPlane.advertisedHost="miniziti-controller-ctrl.${MINIZITI_INGRESS_ZONE}" \
         --set clientApi.advertisedHost="miniziti-controller.${MINIZITI_INGRESS_ZONE}" \
         --set trust-manager.app.trust.namespace="${ZITI_NAMESPACE}" \
         --set trust-manager.enabled=true \

From 34351f3ed742ca4cd262bda26f62f5437f6653d2 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Thu, 22 Feb 2024 17:00:35 -0500
Subject: [PATCH 40/46] add freebsd function defs

---
 common/profiler/cpu_freebsd.go            |  59 ++++++++++++
 router/monitor_freebsd.go                 |  47 ++++++++++
 router/xgress_geneve/listener_freebsd.go  | 108 ++++++++++++++++++++++
 tunnel/intercept/tproxy/tproxy_freebsd.go |  26 ++++++
 4 files changed, 240 insertions(+)
 create mode 100644 common/profiler/cpu_freebsd.go
 create mode 100644 router/monitor_freebsd.go
 create mode 100644 router/xgress_geneve/listener_freebsd.go
 create mode 100644 tunnel/intercept/tproxy/tproxy_freebsd.go

diff --git a/common/profiler/cpu_freebsd.go b/common/profiler/cpu_freebsd.go
new file mode 100644
index 000000000..bd2adc942
--- /dev/null
+++ b/common/profiler/cpu_freebsd.go
@@ -0,0 +1,59 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package profiler
+
+import (
+	"github.com/michaelquigley/pfxlog"
+	"os"
+	"os/signal"
+	"runtime/pprof"
+	"syscall"
+)
+
+type CPU struct {
+	path      string
+	shutdownC <-chan struct{}
+}
+
+func NewCPU(path string) (*CPU, error) {
+	return NewCPUWithShutdown(path, nil)
+}
+
+func NewCPUWithShutdown(path string, shutdownC <-chan struct{}) (*CPU, error) {
+	f, err := os.Create(path)
+	if err != nil {
+		return nil, err
+	}
+	if err := pprof.StartCPUProfile(f); err != nil {
+		return nil, err
+	}
+	pfxlog.Logger().Infof("cpu profiling to [%s]", path)
+	return &CPU{path: path, shutdownC: shutdownC}, nil
+}
+
+func (cpu *CPU) Run() {
+	signalChan := make(chan os.Signal, 1)
+	signal.Notify(signalChan, syscall.SIGUSR2)
+
+	select {
+	case <-signalChan:
+	case <-cpu.shutdownC:
+	}
+
+	pprof.StopCPUProfile()
+	pfxlog.Logger().Info("stopped profiling cpu")
+}
diff --git a/router/monitor_freebsd.go b/router/monitor_freebsd.go
new file mode 100644
index 000000000..aeddf58de
--- /dev/null
+++ b/router/monitor_freebsd.go
@@ -0,0 +1,47 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package router
+
+import (
+	"github.com/michaelquigley/pfxlog"
+	"github.com/openziti/ziti/router/forwarder"
+	"os"
+	"os/signal"
+	"syscall"
+)
+
+type routerMonitor struct {
+	forwarder   *forwarder.Forwarder
+	closeNotify <-chan struct{}
+}
+
+func newRouterMonitor(forwarder *forwarder.Forwarder, closeNotify <-chan struct{}) *routerMonitor {
+	return &routerMonitor{forwarder: forwarder, closeNotify: closeNotify}
+}
+
+func (routerMonitor *routerMonitor) Monitor() {
+	signalChan := make(chan os.Signal, 1)
+	signal.Notify(signalChan, syscall.SIGUSR1)
+	for {
+		select {
+		case <-signalChan:
+			pfxlog.Logger().Info("\n" + routerMonitor.forwarder.Debug())
+		case <-routerMonitor.closeNotify:
+			return
+		}
+	}
+}
diff --git a/router/xgress_geneve/listener_freebsd.go b/router/xgress_geneve/listener_freebsd.go
new file mode 100644
index 000000000..4f9c6ceb9
--- /dev/null
+++ b/router/xgress_geneve/listener_freebsd.go
@@ -0,0 +1,108 @@
+/*
+	Copyright 2019 NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package xgress_geneve
+
+import (
+	"encoding/binary"
+	"net"
+	"syscall"
+
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"github.com/michaelquigley/pfxlog"
+	"github.com/openziti/ziti/router/xgress"
+)
+
+type listener struct{}
+
+func (self *listener) Listen(string, xgress.BindHandler) error {
+	go func() {
+		log := pfxlog.Logger()
+		// Open UDP socket to listen for Geneve Packets
+		conn, err := net.ListenPacket("udp", ":6081")
+		if err != nil {
+			log.WithError(err).Errorf("failed to open geneve interface - udp")
+			// error but return gracefully
+			return
+		}
+		// if no error, will log success
+		log.Infof("geneve interface started successfully - udp: %s", conn.LocalAddr().String())
+		// Close it when done
+		defer conn.Close()
+		// Open a raw socket to send Modified Packets to Networking Stack
+		fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
+		if err != nil {
+			log.WithError(err).Errorf("failed to open geneve interface - fd")
+			// error but return gracefully
+			return
+		}
+		// if no error, will log success
+		log.Infof("geneve interface started successfully - fd: %d", fd)
+		// Close it when done
+		defer syscall.Close(fd)
+		// Loop to process packets
+		for {
+			log := pfxlog.ChannelLogger("geneveListener")
+			buf := make([]byte, 9000)
+			n, _, err := conn.ReadFrom(buf)
+			if err != nil {
+				log.WithError(err).Errorf("error reading from geneve interface - udp")
+				// error but continue to read packets
+				continue
+			}
+			// Remove Geneve layer
+			packet := gopacket.NewPacket(buf[:n], layers.LayerTypeGeneve, gopacket.DecodeOptions{NoCopy: true})
+			if err := packet.ErrorLayer(); err != nil {
+				log.WithError(err.Error()).Errorf("Error decoding some part of the packet")
+				// error but continue to read packets
+				continue
+			}
+			// Extract IP Headers and Payload
+			if ipNetwork := packet.NetworkLayer(); ipNetwork != nil {
+				modifiedPacket := append(ipNetwork.LayerContents(), ipNetwork.LayerPayload()...)
+				// Get Destination IP from the IP Header
+				var array4byte [4]byte
+				copy(array4byte[:], buf[56:60])
+				sockAddress := syscall.SockaddrInet4{
+					Port: 0,
+					Addr: array4byte,
+				}
+				// Print packet details in debug or trace mode
+				log.Tracef("Raw Packet Details: %X", packet)
+				log.Tracef("Raw Modified Packet Details: %X", modifiedPacket)
+				log.Debugf("DIPv4: %v, SPort: %v, DPort: %v", net.IP(buf[56:60]), binary.BigEndian.Uint16(buf[60:62]), binary.BigEndian.Uint16(buf[62:64]))
+				// Send the new packet to be routed to Ziti TProxy
+				err = syscall.Sendto(fd, modifiedPacket, 0, &sockAddress)
+				if err != nil {
+					log.WithError(err).Errorf("failed to send modified packet to geneve interface - fd")
+					// error but continue to send packets
+					continue
+				}
+			} else {
+				log.WithError(err).Errorf("Packet is not an IP Packet")
+				continue
+			}
+		}
+	}()
+	return nil
+}
+
+func (self *listener) Close() error {
+	log := pfxlog.Logger()
+	log.Warn("closing geneve interface")
+	return nil
+}
diff --git a/tunnel/intercept/tproxy/tproxy_freebsd.go b/tunnel/intercept/tproxy/tproxy_freebsd.go
new file mode 100644
index 000000000..38b2c775d
--- /dev/null
+++ b/tunnel/intercept/tproxy/tproxy_freebsd.go
@@ -0,0 +1,26 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package tproxy
+
+import (
+	"github.com/openziti/ziti/tunnel/intercept"
+	"github.com/pkg/errors"
+)
+
+func New(config Config) (intercept.Interceptor, error) {
+	return nil, errors.New("tproxy not supported on darwin")
+}

From be3bd9796736f64ea7e8aa5bba63779f93aabff9 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Tue, 5 Mar 2024 10:43:13 -0500
Subject: [PATCH 41/46] consolidate redundant unix function defs

---
 common/profiler/cpu_darwin.go                 | 59 -------------------
 common/profiler/cpu_linux.go                  | 59 -------------------
 .../profiler/{cpu_freebsd.go => cpu_unix.go}  |  2 +
 common/profiler/cpu_windows.go                |  2 +
 router/monitor_darwin.go                      | 47 ---------------
 router/monitor_linux.go                       | 47 ---------------
 .../{monitor_freebsd.go => monitor_unix.go}   |  2 +
 router/monitor_windows.go                     |  2 +
 tunnel/intercept/tproxy/tproxy_freebsd.go     |  2 +-
 9 files changed, 9 insertions(+), 213 deletions(-)
 delete mode 100644 common/profiler/cpu_darwin.go
 delete mode 100644 common/profiler/cpu_linux.go
 rename common/profiler/{cpu_freebsd.go => cpu_unix.go} (97%)
 delete mode 100644 router/monitor_darwin.go
 delete mode 100644 router/monitor_linux.go
 rename router/{monitor_freebsd.go => monitor_unix.go} (97%)

diff --git a/common/profiler/cpu_darwin.go b/common/profiler/cpu_darwin.go
deleted file mode 100644
index 1f46c466a..000000000
--- a/common/profiler/cpu_darwin.go
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
-	Copyright NetFoundry Inc.
-
-	Licensed under the Apache License, Version 2.0 (the "License");
-	you may not use this file except in compliance with the License.
-	You may obtain a copy of the License at
-
-	https://www.apache.org/licenses/LICENSE-2.0
-
-	Unless required by applicable law or agreed to in writing, software
-	distributed under the License is distributed on an "AS IS" BASIS,
-	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-	See the License for the specific language governing permissions and
-	limitations under the License.
-*/
-
-package profiler
-
-import (
-	"github.com/michaelquigley/pfxlog"
-	"os"
-	"os/signal"
-	"runtime/pprof"
-	"syscall"
-)
-
-type CPU struct {
-	path      string
-	shutdownC chan struct{}
-}
-
-func NewCPU(path string) (*CPU, error) {
-	return NewCPUWithShutdown(path, nil)
-}
-
-func NewCPUWithShutdown(path string, shutdownC chan struct{}) (*CPU, error) {
-	f, err := os.Create(path)
-	if err != nil {
-		return nil, err
-	}
-	if err := pprof.StartCPUProfile(f); err != nil {
-		return nil, err
-	}
-	pfxlog.Logger().Infof("cpu profiling to [%s]", path)
-	return &CPU{path: path, shutdownC: shutdownC}, nil
-}
-
-func (cpu *CPU) Run() {
-	signalChan := make(chan os.Signal, 1)
-	signal.Notify(signalChan, syscall.SIGUSR2)
-
-	select {
-	case <-signalChan:
-	case <-cpu.shutdownC:
-	}
-
-	pprof.StopCPUProfile()
-	pfxlog.Logger().Info("stopped profiling cpu")
-}
diff --git a/common/profiler/cpu_linux.go b/common/profiler/cpu_linux.go
deleted file mode 100644
index bd2adc942..000000000
--- a/common/profiler/cpu_linux.go
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
-	Copyright NetFoundry Inc.
-
-	Licensed under the Apache License, Version 2.0 (the "License");
-	you may not use this file except in compliance with the License.
-	You may obtain a copy of the License at
-
-	https://www.apache.org/licenses/LICENSE-2.0
-
-	Unless required by applicable law or agreed to in writing, software
-	distributed under the License is distributed on an "AS IS" BASIS,
-	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-	See the License for the specific language governing permissions and
-	limitations under the License.
-*/
-
-package profiler
-
-import (
-	"github.com/michaelquigley/pfxlog"
-	"os"
-	"os/signal"
-	"runtime/pprof"
-	"syscall"
-)
-
-type CPU struct {
-	path      string
-	shutdownC <-chan struct{}
-}
-
-func NewCPU(path string) (*CPU, error) {
-	return NewCPUWithShutdown(path, nil)
-}
-
-func NewCPUWithShutdown(path string, shutdownC <-chan struct{}) (*CPU, error) {
-	f, err := os.Create(path)
-	if err != nil {
-		return nil, err
-	}
-	if err := pprof.StartCPUProfile(f); err != nil {
-		return nil, err
-	}
-	pfxlog.Logger().Infof("cpu profiling to [%s]", path)
-	return &CPU{path: path, shutdownC: shutdownC}, nil
-}
-
-func (cpu *CPU) Run() {
-	signalChan := make(chan os.Signal, 1)
-	signal.Notify(signalChan, syscall.SIGUSR2)
-
-	select {
-	case <-signalChan:
-	case <-cpu.shutdownC:
-	}
-
-	pprof.StopCPUProfile()
-	pfxlog.Logger().Info("stopped profiling cpu")
-}
diff --git a/common/profiler/cpu_freebsd.go b/common/profiler/cpu_unix.go
similarity index 97%
rename from common/profiler/cpu_freebsd.go
rename to common/profiler/cpu_unix.go
index bd2adc942..abda81781 100644
--- a/common/profiler/cpu_freebsd.go
+++ b/common/profiler/cpu_unix.go
@@ -1,3 +1,5 @@
+//go:build linux || darwin || freebsd
+
 /*
 	Copyright NetFoundry Inc.
 
diff --git a/common/profiler/cpu_windows.go b/common/profiler/cpu_windows.go
index db9e8f083..d3cb2585b 100644
--- a/common/profiler/cpu_windows.go
+++ b/common/profiler/cpu_windows.go
@@ -1,3 +1,5 @@
+//go:build windows
+
 /*
 	Copyright NetFoundry Inc.
 
diff --git a/router/monitor_darwin.go b/router/monitor_darwin.go
deleted file mode 100644
index aeddf58de..000000000
--- a/router/monitor_darwin.go
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-	Copyright NetFoundry Inc.
-
-	Licensed under the Apache License, Version 2.0 (the "License");
-	you may not use this file except in compliance with the License.
-	You may obtain a copy of the License at
-
-	https://www.apache.org/licenses/LICENSE-2.0
-
-	Unless required by applicable law or agreed to in writing, software
-	distributed under the License is distributed on an "AS IS" BASIS,
-	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-	See the License for the specific language governing permissions and
-	limitations under the License.
-*/
-
-package router
-
-import (
-	"github.com/michaelquigley/pfxlog"
-	"github.com/openziti/ziti/router/forwarder"
-	"os"
-	"os/signal"
-	"syscall"
-)
-
-type routerMonitor struct {
-	forwarder   *forwarder.Forwarder
-	closeNotify <-chan struct{}
-}
-
-func newRouterMonitor(forwarder *forwarder.Forwarder, closeNotify <-chan struct{}) *routerMonitor {
-	return &routerMonitor{forwarder: forwarder, closeNotify: closeNotify}
-}
-
-func (routerMonitor *routerMonitor) Monitor() {
-	signalChan := make(chan os.Signal, 1)
-	signal.Notify(signalChan, syscall.SIGUSR1)
-	for {
-		select {
-		case <-signalChan:
-			pfxlog.Logger().Info("\n" + routerMonitor.forwarder.Debug())
-		case <-routerMonitor.closeNotify:
-			return
-		}
-	}
-}
diff --git a/router/monitor_linux.go b/router/monitor_linux.go
deleted file mode 100644
index aeddf58de..000000000
--- a/router/monitor_linux.go
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-	Copyright NetFoundry Inc.
-
-	Licensed under the Apache License, Version 2.0 (the "License");
-	you may not use this file except in compliance with the License.
-	You may obtain a copy of the License at
-
-	https://www.apache.org/licenses/LICENSE-2.0
-
-	Unless required by applicable law or agreed to in writing, software
-	distributed under the License is distributed on an "AS IS" BASIS,
-	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-	See the License for the specific language governing permissions and
-	limitations under the License.
-*/
-
-package router
-
-import (
-	"github.com/michaelquigley/pfxlog"
-	"github.com/openziti/ziti/router/forwarder"
-	"os"
-	"os/signal"
-	"syscall"
-)
-
-type routerMonitor struct {
-	forwarder   *forwarder.Forwarder
-	closeNotify <-chan struct{}
-}
-
-func newRouterMonitor(forwarder *forwarder.Forwarder, closeNotify <-chan struct{}) *routerMonitor {
-	return &routerMonitor{forwarder: forwarder, closeNotify: closeNotify}
-}
-
-func (routerMonitor *routerMonitor) Monitor() {
-	signalChan := make(chan os.Signal, 1)
-	signal.Notify(signalChan, syscall.SIGUSR1)
-	for {
-		select {
-		case <-signalChan:
-			pfxlog.Logger().Info("\n" + routerMonitor.forwarder.Debug())
-		case <-routerMonitor.closeNotify:
-			return
-		}
-	}
-}
diff --git a/router/monitor_freebsd.go b/router/monitor_unix.go
similarity index 97%
rename from router/monitor_freebsd.go
rename to router/monitor_unix.go
index aeddf58de..d600ae387 100644
--- a/router/monitor_freebsd.go
+++ b/router/monitor_unix.go
@@ -1,3 +1,5 @@
+//go:build linux || darwin || freebsd
+
 /*
 	Copyright NetFoundry Inc.
 
diff --git a/router/monitor_windows.go b/router/monitor_windows.go
index 9d38eb4c3..4c9c5b998 100644
--- a/router/monitor_windows.go
+++ b/router/monitor_windows.go
@@ -1,3 +1,5 @@
+//go:build windows
+
 /*
 	Copyright NetFoundry Inc.
 
diff --git a/tunnel/intercept/tproxy/tproxy_freebsd.go b/tunnel/intercept/tproxy/tproxy_freebsd.go
index 38b2c775d..3f7ea2433 100644
--- a/tunnel/intercept/tproxy/tproxy_freebsd.go
+++ b/tunnel/intercept/tproxy/tproxy_freebsd.go
@@ -22,5 +22,5 @@ import (
 )
 
 func New(config Config) (intercept.Interceptor, error) {
-	return nil, errors.New("tproxy not supported on darwin")
+	return nil, errors.New("tproxy not supported on FreeBSD")
 }

From 499bacbe019a7641efc7799356b95f4663e1aa42 Mon Sep 17 00:00:00 2001
From: Kenneth Bingham <kenneth.bingham@netfoundry.io>
Date: Wed, 6 Mar 2024 15:09:19 -0500
Subject: [PATCH 42/46] describe old and odd fablab VPCs

---
 zititest/scripts/housekeeper-aws.bash | 107 ++++++++++++++++++++++++--
 1 file changed, 100 insertions(+), 7 deletions(-)

diff --git a/zititest/scripts/housekeeper-aws.bash b/zititest/scripts/housekeeper-aws.bash
index f03f3d737..8465acd8a 100644
--- a/zititest/scripts/housekeeper-aws.bash
+++ b/zititest/scripts/housekeeper-aws.bash
@@ -9,12 +9,13 @@ function describe_instances() {
   local state=$2
   for region in us-east-1 us-west-2
   do
-    local outfile="aged-fablab-instances-${region}.json"
+    local old_file="old-fablab-${state}-instances-${region}.json"
     aws --region "$region" ec2 describe-instances \
       --filters "Name=instance-state-name,Values=${state}" \
                 "Name=tag:source,Values=fablab" \
       --query   "Reservations[*].Instances[*].{InstanceId:InstanceId,LaunchTime:LaunchTime,State:State.Name,Tags:Tags}" \
-    | jq -r \
+    | jq \
+        --raw-output \
         --arg region "$region" \
         --arg oldest "$oldest" '
         [
@@ -24,15 +25,87 @@ function describe_instances() {
           |{InstanceId: .InstanceId, Region: $region, LaunchTime: .LaunchTime, State: .State, Tags: .Tags}
         ]
       ' \
-    | tee $outfile \
-    | jq '.|length' | xargs -ILEN echo "Described LEN aged instances in $region in $(realpath $outfile)"
+    | tee $old_file \
+    | jq 'length' | xargs -ILEN echo "Described LEN old instances in $region in $(realpath $old_file)"
+  done
+}
+
+function describe_vpcs {
+  cd "$(mktemp -d)"
+  local oldest=$1
+  for region in us-east-1 us-west-2
+  do
+    local old_file="old-fablab-vpcs-${region}.json"
+    local odd_file="odd-fablab-vpcs-${region}.json"
+    local -A vpc_create_events=() odd_vpcs=() old_vpcs=()
+    read -ra all_fablab_vpcs < <(
+      # shellcheck disable=SC2016
+      aws --region "$region" ec2 describe-vpcs \
+          --query 'Vpcs[?Tags[?Key==`source` && Value==`fablab`]].VpcId' \
+          --output text
+    )
+    if [[ ${#all_fablab_vpcs[@]} -ge 1 ]]
+    then
+      for vpc_id in "${all_fablab_vpcs[@]}"
+      do
+        vpc_create_events["$vpc_id"]=$(
+          # shellcheck disable=SC2016
+          aws cloudtrail lookup-events \
+            --region $region \
+            --lookup-attributes "AttributeKey=ResourceName,AttributeValue=${vpc_id}" \
+            --query 'Events[?EventName==`CreateVpc`].CloudTrailEvent'
+        )
+      done
+
+      for vpc_id in "${all_fablab_vpcs[@]}"
+      do
+        if [[ "$(jq 'length' <<< "${vpc_create_events[$vpc_id]}")" -ne 1 ]]
+        then
+          odd_vpcs["$vpc_id"]="true"
+        else
+          old_vpcs["$vpc_id"]=$(
+            jq \
+              --raw-output \
+              --arg oldest "$oldest" '
+                [
+                  .[]
+                  |fromjson
+                  |select(.eventTime < $oldest)
+                  |{eventName: .eventName, eventTime: .eventTime, awsRegion: .awsRegion, vpcId: .responseElements.vpc.vpcId}
+                ]
+              ' <<< "${vpc_create_events[$vpc_id]}"
+          )
+        fi
+      done
+
+      # for each key in the old_vpcs array
+      local old_vpcs_json='[]'
+      for vpc_id in "${!old_vpcs[@]}"
+      do
+        if [[ "$(jq 'length' <<< "${old_vpcs[$vpc_id]}")" -eq 1 ]]
+        then
+          old_vpcs_json=$(jq --argjson append "${old_vpcs[$vpc_id]}" '. += $append' <<< "${old_vpcs_json}")
+        fi
+      done
+      tee "$old_file" <<< "$old_vpcs_json" \
+      | jq 'length' | xargs -ILEN echo "Described LEN old VPCs in $region in $(realpath $old_file)"
+
+      # for each key in the odd_vpcs array
+      local odd_vpcs_json='[]'
+      for vpc_id in "${!odd_vpcs[@]}"
+      do
+        odd_vpcs_json=$(jq --arg vpc_id "$vpc_id" '. += [{vpcId: $vpc_id}]' <<< "${odd_vpcs_json}")
+      done
+      tee "$odd_file" <<< "$odd_vpcs_json" \
+      | jq 'length' | xargs -ILEN echo "Described LEN odd VPCs in $region in $(realpath $odd_file)"
+    fi
   done
 }
 
 function stop_instances(){
   local stopfile onecount region instanceid
   stopfile=$1
-  onecount=$(jq '.|length' "$stopfile")
+  onecount=$(jq 'length' "$stopfile")
   for i in $(seq 0 $((onecount-1)))
   do
     region=$(jq -r ".[$i].Region" "$stopfile")
@@ -45,7 +118,7 @@ function stop_instances(){
 function terminate_instances(){
   local stopfile onecount region instanceid
   stopfile=$1
-  onecount=$(jq '.|length' "$stopfile")
+  onecount=$(jq 'length' "$stopfile")
   for i in $(seq 0 $((onecount-1)))
   do
     region=$(jq -r ".[$i].Region" "$stopfile")
@@ -79,6 +152,16 @@ do
       terminate_instances "${2:-}"
       exit
       ;;
+    describe)
+      if [[ "${2:-}" =~ ^(instance|vpc)s?$ ]]
+      then
+        typeset -a DESCRIBE=("${2}")
+        shift 2
+      else
+        typeset -a DESCRIBE=(instances vpcs)
+        shift 1
+      fi
+      ;;
     --age)
       AGE="${2:-}"
       if ! [[ "$AGE" =~ ^[0-9]+$ ]];
@@ -105,4 +188,14 @@ do
   esac
 done
 
-describe_instances "$(date -d "-${AGE:-7} days" -Id)" "${STATE:-running}"
+for describe in "${DESCRIBE[@]}"
+do
+  case "$describe" in
+    instance*)
+      describe_instances "$(date -d "-${AGE:-7} days" -Id)" "${STATE:-running}"
+      ;;
+    vpc*)
+      describe_vpcs "$(date -d "-${AGE:-7} days" -Id)"
+      ;;
+  esac
+done

From fab81e4cd46e584c0ab87188ce9182926085ba53 Mon Sep 17 00:00:00 2001
From: Paul Lorenz <paul.lorenz@netfoundry.io>
Date: Fri, 16 Feb 2024 17:50:13 -0500
Subject: [PATCH 43/46] Add terminator chaos testing and fix issues found.
 Fixes #1794 Fixes #1369

---
 CHANGELOG.md                                  |  41 +
 common/ctrl_msg/messages.go                   |   4 +
 common/handler_common/common.go               |  18 +
 ...rcuit_detail.go => circuit_inspections.go} |   0
 ...inspect_result.go => links_inspections.go} |   0
 common/inspect/router_message_inspections.go  |  41 +
 common/inspect/terminator_inspections.go      |  39 +
 common/pb/cmd_pb/cmd.pb.go                    |   2 +-
 common/pb/ctrl_pb/ctrl.pb.go                  |  69 +-
 common/pb/ctrl_pb/ctrl.proto                  |   2 +
 common/pb/edge_cmd_pb/edge_cmd.pb.go          |   2 +-
 common/pb/edge_ctrl_pb/edge_ctrl.pb.go        |  33 +-
 common/pb/edge_ctrl_pb/edge_ctrl.proto        |   1 +
 common/pb/edge_mgmt_pb/edge_mgmt.pb.go        |   2 +-
 common/pb/mgmt_pb/impl.go                     |  12 +
 common/pb/mgmt_pb/mgmt.pb.go                  | 726 ++++++++++---
 common/pb/mgmt_pb/mgmt.proto                  |  32 +
 controller/api_impl/circuit_api_model.go      |  17 +-
 controller/change/util.go                     |  30 +
 controller/command/command.go                 |   5 +
 controller/command/rate_limiter.go            | 337 +++++-
 controller/command/rate_limiter_test.go       |  87 +-
 controller/config/config.go                   |  57 +-
 controller/db/api_session_store.go            |  53 +-
 controller/db/stores.go                       |   9 +-
 controller/db/testing.go                      |   3 +-
 controller/events/metrics_mappers.go          |   7 +-
 controller/handler_ctrl/base.go               |   9 +-
 controller/handler_ctrl/bind.go               |   5 +
 controller/handler_ctrl/remove_terminators.go |   5 +-
 .../handler_edge_ctrl/create_terminator_v2.go |  15 +-
 controller/handler_edge_ctrl/errors.go        |  16 +
 controller/handler_edge_ctrl/errors_test.go   |  29 +
 controller/handler_mgmt/bind.go               |   6 +
 .../validate_router_sdk_terminators.go        |  88 ++
 .../internal/routes/authenticate_router.go    |  26 +-
 controller/internal/routes/identity_router.go |  20 +-
 controller/internal/routes/service_router.go  |  21 +-
 controller/model/identity_manager.go          |   4 +-
 controller/network/network.go                 |  71 +-
 controller/network/router.go                  | 100 ++
 controller/network/router_messaging.go        | 362 ++++++-
 controller/raft/member.go                     |  12 +-
 controller/raft/raft.go                       |   4 +
 go.mod                                        |  22 +-
 go.sum                                        |  42 +-
 router/config.go                              |  59 ++
 router/env/env.go                             |   2 +
 router/fabric/manager.go                      |   7 +-
 router/handler_ctrl/bind.go                   |  38 +-
 router/handler_ctrl/inspect.go                |  27 +-
 .../handler_ctrl/validate_terminators_v2.go   |  84 +-
 router/router.go                              |   7 +
 router/router_test.go                         |   3 +
 router/xgress/request.go                      |   4 +-
 router/xgress/xgress.go                       |   4 +
 router/xgress_edge/dialer.go                  |  12 +-
 router/xgress_edge/fabric.go                  | 202 ++--
 router/xgress_edge/hosted.go                  | 959 ++++++++++++++----
 router/xgress_edge/listener.go                | 180 ++--
 router/xgress_edge/terminator_heap.go         |  45 -
 version                                       |   2 +-
 ziti/cmd/cmd.go                               |   2 +-
 ziti/cmd/database/add_debug_admin.go          |   4 +-
 ziti/cmd/edge/root.go                         |  24 +-
 ziti/cmd/edge/validate_service_hosting.go     | 131 +++
 ziti/cmd/fabric/inspect.go                    |   7 +-
 ziti/cmd/fabric/root.go                       |   1 +
 .../fabric/validate_router_sdk_terminators.go | 158 +++
 ziti/cmd/fabric/validate_terminators.go       |  10 +-
 zititest/go.mod                               |  24 +-
 zititest/go.sum                               |  46 +-
 .../db-sdk-hosting-test/configs/ctrl.yml.tmpl | 205 ++++
 .../configs/router.yml.tmpl                   |  70 ++
 zititest/models/db-sdk-hosting-test/main.go   | 324 ++++++
 zititest/models/links-test/main.go            |   1 -
 .../sdk-hosting-test/configs/ctrl.yml.tmpl    |   4 +
 zititest/models/sdk-hosting-test/main.go      | 444 ++++----
 .../models/sdk-hosting-test/validation.go     | 263 +++++
 .../zitilab/actions/edge/init_identities.go   |   2 +-
 zititest/zitilab/cli/cli.go                   |  41 +
 zititest/zitilab/component_common.go          |   3 +-
 zititest/zitilab/component_ziti_tunnel.go     |  19 +-
 zititest/zitilab/component_zrok_looptest.go   |   2 +-
 zititest/zitilab/models/db_builder.go         |   3 +-
 85 files changed, 4825 insertions(+), 1084 deletions(-)
 rename common/inspect/{circuit_detail.go => circuit_inspections.go} (100%)
 rename common/inspect/{links_inspect_result.go => links_inspections.go} (100%)
 create mode 100644 common/inspect/router_message_inspections.go
 create mode 100644 common/inspect/terminator_inspections.go
 create mode 100644 controller/change/util.go
 create mode 100644 controller/handler_edge_ctrl/errors_test.go
 create mode 100644 controller/handler_mgmt/validate_router_sdk_terminators.go
 delete mode 100644 router/xgress_edge/terminator_heap.go
 create mode 100644 ziti/cmd/edge/validate_service_hosting.go
 create mode 100644 ziti/cmd/fabric/validate_router_sdk_terminators.go
 create mode 100644 zititest/models/db-sdk-hosting-test/configs/ctrl.yml.tmpl
 create mode 100644 zititest/models/db-sdk-hosting-test/configs/router.yml.tmpl
 create mode 100644 zititest/models/db-sdk-hosting-test/main.go
 create mode 100644 zititest/models/sdk-hosting-test/validation.go

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 358d48d86..9080be82f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,44 @@
+# Release 0.33.0
+
+# What's New
+
+* SDK Terminator stability improvements
+* Minor feature updates and bug fixes
+
+## SDK Terminator stability improvements
+
+This release was focused on creating a chaos test for SDK terminators, running it and fixing any issues found.
+The test repeatedly and randomly restarts the controller, routers and tunnelers then verifies that terminators
+end up in the correct state. 
+
+The following tools were also used/added to aid in diagnosing and fixing issues:
+
+* `ziti fabric validate router-sdk-terminators` 
+    * Compares the controller state with the router state
+* `ziti fabric validate terminators`
+    * Checks each selected terminator to ensure it's still valid on the router and/or sdk
+* `ziti fabric inspect sdk-terminators`
+    * Allows inspecting each routers terminator state
+* `ziti fabric inspect router-messaging`
+    * Allows inspecting what the controller has queued for router state sync and terminator validations
+* `ziti edge validate service-hosting`
+    * Shows how many terminators each identity which can host a service has
+
+Several changes were made to the terminator code to ensure that terminators are properly created and cleaned up.
+The routers now use an adaptive rate limiter to control how fast they send terminator related requests to the
+controller. For this to work properly, the rate limiting on the controller must be enabled, so it can report
+back to the routers when it's got too much work.
+
+## Component Updates and Bug Fixes
+
+* github.com/openziti/edge-api: [v0.26.10 -> v0.26.12](https://github.com/openziti/edge-api/compare/v0.26.10...v0.26.12)
+* github.com/openziti/ziti: [v0.32.2 -> v0.33.0](https://github.com/openziti/ziti/compare/v0.32.2...v0.33.0)
+    * [Issue #1794](https://github.com/openziti/ziti/issues/1794) - Add SDK terminator chaos test and fix any bugs found as part of chaos testing
+    * [Issue #1369](https://github.com/openziti/ziti/issues/1369) - Allow filtering by policy type when listing identities for service or services for identity
+    * [Issue #1204](https://github.com/openziti/ziti/issues/1204) - ziti cli identity tags related flags misbehaving
+    * [Issue #987](https://github.com/openziti/ziti/issues/987) - "ziti create config router edge" doesn't know about --tunnelerMode proxy
+    * [Issue #652](https://github.com/openziti/ziti/issues/652) - Update CLI script M1 Support when github actions allows
+
 # Release 0.32.2
 
 ## What's New
diff --git a/common/ctrl_msg/messages.go b/common/ctrl_msg/messages.go
index 2b7191c44..96a8524ab 100644
--- a/common/ctrl_msg/messages.go
+++ b/common/ctrl_msg/messages.go
@@ -55,6 +55,10 @@ const (
 	CreateCircuitRespCircuitId  = 11
 	CreateCircuitRespAddress    = 12
 	CreateCircuitRespTagsHeader = 13
+
+	HeaderResultErrorCode = 10
+
+	ResultErrorRateLimited = 1
 )
 
 func NewCircuitSuccessMsg(sessionId, address string) *channel.Message {
diff --git a/common/handler_common/common.go b/common/handler_common/common.go
index 2993c5f6d..57db44179 100644
--- a/common/handler_common/common.go
+++ b/common/handler_common/common.go
@@ -3,6 +3,7 @@ package handler_common
 import (
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/channel/v2"
+	"github.com/openziti/ziti/common/ctrl_msg"
 	"time"
 )
 
@@ -39,3 +40,20 @@ func SendOpResult(request *channel.Message, ch channel.Channel, op string, messa
 		log.WithError(err).Error("failed to send result")
 	}
 }
+
+func SendServerBusy(request *channel.Message, ch channel.Channel, op string) {
+	log := pfxlog.ContextLogger(ch.Label()).WithField("operation", op)
+	log.Errorf("%v error performing %v: (%s)", ch.LogicalName(), op, "server too busy")
+
+	response := channel.NewResult(false, "server too busy")
+	response.ReplyTo(request)
+	response.Headers.PutUint32Header(ctrl_msg.HeaderResultErrorCode, ctrl_msg.ResultErrorRateLimited)
+	if err := response.WithTimeout(5 * time.Second).SendAndWaitForWire(ch); err != nil {
+		log.WithError(err).Error("failed to send result")
+	}
+}
+
+func WasRateLimited(msg *channel.Message) bool {
+	val, found := msg.GetUint32Header(ctrl_msg.HeaderResultErrorCode)
+	return found && val == ctrl_msg.ResultErrorRateLimited
+}
diff --git a/common/inspect/circuit_detail.go b/common/inspect/circuit_inspections.go
similarity index 100%
rename from common/inspect/circuit_detail.go
rename to common/inspect/circuit_inspections.go
diff --git a/common/inspect/links_inspect_result.go b/common/inspect/links_inspections.go
similarity index 100%
rename from common/inspect/links_inspect_result.go
rename to common/inspect/links_inspections.go
diff --git a/common/inspect/router_message_inspections.go b/common/inspect/router_message_inspections.go
new file mode 100644
index 000000000..1f069d115
--- /dev/null
+++ b/common/inspect/router_message_inspections.go
@@ -0,0 +1,41 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package inspect
+
+type RouterMessagingState struct {
+	RouterUpdates         []*RouterUpdates         `json:"routerUpdates"`
+	TerminatorValidations []*TerminatorValidations `json:"terminatorValidations"`
+}
+
+type RouterInfo struct {
+	Id   string `json:"id"`
+	Name string `json:"name"`
+}
+
+type RouterUpdates struct {
+	Router         RouterInfo   `json:"router"`
+	Version        uint32       `json:"version"`
+	ChangedRouters []RouterInfo `json:"changedRouters"`
+	SendInProgress bool         `json:"sendInProgress"`
+}
+
+type TerminatorValidations struct {
+	Router          RouterInfo `json:"router"`
+	Terminators     []string   `json:"terminators"`
+	CheckInProgress bool       `json:"checkInProgress"`
+	LastSend        string     `json:"lastSend"`
+}
diff --git a/common/inspect/terminator_inspections.go b/common/inspect/terminator_inspections.go
new file mode 100644
index 000000000..f12391fde
--- /dev/null
+++ b/common/inspect/terminator_inspections.go
@@ -0,0 +1,39 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package inspect
+
+type SdkTerminatorInspectResult struct {
+	Entries []*SdkTerminatorInspectDetail `json:"entries"`
+	Errors  []string                      `json:"errors"`
+}
+
+type SdkTerminatorInspectDetail struct {
+	Key             string `json:"key"`
+	Id              string `json:"id"`
+	State           string `json:"state"`
+	Token           string `json:"token"`
+	ListenerId      string `json:"listenerId"`
+	Instance        string `json:"instance"`
+	Cost            uint16 `json:"cost"`
+	Precedence      string `json:"precedence"`
+	AssignIds       bool   `json:"assignIds"`
+	V2              bool   `json:"v2"`
+	SupportsInspect bool   `json:"supportsInspect"`
+	OperationActive bool   `json:"establishActive"`
+	CreateTime      string `json:"createTime"`
+	LastAttempt     string `json:"lastAttempt"`
+}
diff --git a/common/pb/cmd_pb/cmd.pb.go b/common/pb/cmd_pb/cmd.pb.go
index fb9961620..8f17c8487 100644
--- a/common/pb/cmd_pb/cmd.pb.go
+++ b/common/pb/cmd_pb/cmd.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v4.23.4
+// 	protoc        v3.21.12
 // source: cmd.proto
 
 package cmd_pb
diff --git a/common/pb/ctrl_pb/ctrl.pb.go b/common/pb/ctrl_pb/ctrl.pb.go
index 7b258309c..df1085a8a 100644
--- a/common/pb/ctrl_pb/ctrl.pb.go
+++ b/common/pb/ctrl_pb/ctrl.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v4.23.4
+// 	protoc        v3.21.12
 // source: ctrl.proto
 
 package ctrl_pb
@@ -923,6 +923,7 @@ type Terminator struct {
 	Id      string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
 	Binding string `protobuf:"bytes,2,opt,name=binding,proto3" json:"binding,omitempty"`
 	Address string `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"`
+	Marker  uint64 `protobuf:"varint,4,opt,name=marker,proto3" json:"marker,omitempty"`
 }
 
 func (x *Terminator) Reset() {
@@ -978,6 +979,13 @@ func (x *Terminator) GetAddress() string {
 	return ""
 }
 
+func (x *Terminator) GetMarker() uint64 {
+	if x != nil {
+		return x.Marker
+	}
+	return 0
+}
+
 type ValidateTerminatorsRequest struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -1088,6 +1096,7 @@ type RouterTerminatorState struct {
 	Valid  bool                    `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"`
 	Reason TerminatorInvalidReason `protobuf:"varint,2,opt,name=reason,proto3,enum=ziti.ctrl.pb.TerminatorInvalidReason" json:"reason,omitempty"`
 	Detail string                  `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` // inspect info if valid
+	Marker uint64                  `protobuf:"varint,4,opt,name=marker,proto3" json:"marker,omitempty"`
 }
 
 func (x *RouterTerminatorState) Reset() {
@@ -1143,6 +1152,13 @@ func (x *RouterTerminatorState) GetDetail() string {
 	return ""
 }
 
+func (x *RouterTerminatorState) GetMarker() uint64 {
+	if x != nil {
+		return x.Marker
+	}
+	return 0
+}
+
 type ValidateTerminatorsV2Response struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -2647,34 +2663,37 @@ var file_ctrl_proto_rawDesc = []byte{
 	0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
 	0x73, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
 	0x49, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x74, 0x65, 0x72, 0x6d, 0x69,
-	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x22, 0x50, 0x0a, 0x0a, 0x54, 0x65, 0x72, 0x6d,
+	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x22, 0x68, 0x0a, 0x0a, 0x54, 0x65, 0x72, 0x6d,
 	0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
 	0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e,
 	0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67,
 	0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x58, 0x0a, 0x1a, 0x56, 0x61,
-	0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
-	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0b, 0x74, 0x65, 0x72, 0x6d,
-	0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e,
-	0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72,
-	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x0b, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
-	0x74, 0x6f, 0x72, 0x73, 0x22, 0x7a, 0x0a, 0x1c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65,
-	0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x56, 0x32, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0b, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
-	0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x7a, 0x69, 0x74, 0x69,
-	0x2e, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
-	0x74, 0x6f, 0x72, 0x52, 0x0b, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73,
-	0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x78, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x78, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64,
-	0x22, 0x84, 0x01, 0x0a, 0x15, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x54, 0x65, 0x72, 0x6d, 0x69,
-	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
-	0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64,
-	0x12, 0x3d, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
-	0x32, 0x25, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e,
-	0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69,
-	0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12,
-	0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
-	0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x22, 0xd0, 0x01, 0x0a, 0x1d, 0x56, 0x61, 0x6c, 0x69,
+	0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x61,
+	0x72, 0x6b, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x61, 0x72, 0x6b,
+	0x65, 0x72, 0x22, 0x58, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65,
+	0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x12, 0x3a, 0x0a, 0x0b, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x74, 0x72,
+	0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52,
+	0x0b, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x7a, 0x0a, 0x1c,
+	0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
+	0x6f, 0x72, 0x73, 0x56, 0x32, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3a, 0x0a, 0x0b,
+	0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x18, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62,
+	0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x0b, 0x74, 0x65, 0x72,
+	0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x66, 0x69, 0x78, 0x49,
+	0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69,
+	0x78, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x22, 0x9c, 0x01, 0x0a, 0x15, 0x52, 0x6f, 0x75,
+	0x74, 0x65, 0x72, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61,
+	0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x3d, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73,
+	0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e,
+	0x63, 0x74, 0x72, 0x6c, 0x2e, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
+	0x6f, 0x72, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52,
+	0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69,
+	0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12,
+	0x16, 0x0a, 0x06, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52,
+	0x06, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x22, 0xd0, 0x01, 0x0a, 0x1d, 0x56, 0x61, 0x6c, 0x69,
 	0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x56,
 	0x32, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x73, 0x74, 0x61,
 	0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x7a, 0x69, 0x74, 0x69,
diff --git a/common/pb/ctrl_pb/ctrl.proto b/common/pb/ctrl_pb/ctrl.proto
index 3d1d1d954..eb64a1be2 100644
--- a/common/pb/ctrl_pb/ctrl.proto
+++ b/common/pb/ctrl_pb/ctrl.proto
@@ -109,6 +109,7 @@ message Terminator {
   string id = 1;
   string binding = 2;
   string address = 3;
+  uint64 marker = 4;
 }
 
 message ValidateTerminatorsRequest {
@@ -130,6 +131,7 @@ message RouterTerminatorState {
   bool valid = 1;
   TerminatorInvalidReason reason = 2;
   string detail = 3; // inspect info if valid
+  uint64 marker = 4;
 }
 
 message ValidateTerminatorsV2Response {
diff --git a/common/pb/edge_cmd_pb/edge_cmd.pb.go b/common/pb/edge_cmd_pb/edge_cmd.pb.go
index 3918f58a1..dfa6c0220 100644
--- a/common/pb/edge_cmd_pb/edge_cmd.pb.go
+++ b/common/pb/edge_cmd_pb/edge_cmd.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v4.23.4
+// 	protoc        v3.21.12
 // source: edge_cmd.proto
 
 package edge_cmd_pb
diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go
index a50e159cc..b47cbaff0 100644
--- a/common/pb/edge_ctrl_pb/edge_ctrl.pb.go
+++ b/common/pb/edge_ctrl_pb/edge_ctrl.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v4.23.4
+// 	protoc        v3.21.12
 // source: edge_ctrl.proto
 
 package edge_ctrl_pb
@@ -384,10 +384,11 @@ func (TerminatorPrecedence) EnumDescriptor() ([]byte, []int) {
 type CreateTerminatorResult int32
 
 const (
-	CreateTerminatorResult_Success          CreateTerminatorResult = 0
-	CreateTerminatorResult_FailedIdConflict CreateTerminatorResult = 1
-	CreateTerminatorResult_FailedOther      CreateTerminatorResult = 2
-	CreateTerminatorResult_FailedBusy       CreateTerminatorResult = 3
+	CreateTerminatorResult_Success              CreateTerminatorResult = 0
+	CreateTerminatorResult_FailedIdConflict     CreateTerminatorResult = 1
+	CreateTerminatorResult_FailedOther          CreateTerminatorResult = 2
+	CreateTerminatorResult_FailedBusy           CreateTerminatorResult = 3
+	CreateTerminatorResult_FailedInvalidSession CreateTerminatorResult = 4
 )
 
 // Enum value maps for CreateTerminatorResult.
@@ -397,12 +398,14 @@ var (
 		1: "FailedIdConflict",
 		2: "FailedOther",
 		3: "FailedBusy",
+		4: "FailedInvalidSession",
 	}
 	CreateTerminatorResult_value = map[string]int32{
-		"Success":          0,
-		"FailedIdConflict": 1,
-		"FailedOther":      2,
-		"FailedBusy":       3,
+		"Success":              0,
+		"FailedIdConflict":     1,
+		"FailedOther":          2,
+		"FailedBusy":           3,
+		"FailedInvalidSession": 4,
 	}
 )
 
@@ -4113,16 +4116,18 @@ var file_edge_ctrl_proto_rawDesc = []byte{
 	0x0a, 0x14, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x65, 0x63,
 	0x65, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c,
 	0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x10,
-	0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x5c, 0x0a,
+	0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x76, 0x0a,
 	0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
 	0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x75, 0x63, 0x63, 0x65,
 	0x73, 0x73, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x49, 0x64,
 	0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x61,
 	0x69, 0x6c, 0x65, 0x64, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x46,
-	0x61, 0x69, 0x6c, 0x65, 0x64, 0x42, 0x75, 0x73, 0x79, 0x10, 0x03, 0x42, 0x2a, 0x5a, 0x28, 0x67,
-	0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a, 0x69,
-	0x74, 0x69, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x5f,
-	0x63, 0x74, 0x72, 0x6c, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x61, 0x69, 0x6c, 0x65, 0x64, 0x42, 0x75, 0x73, 0x79, 0x10, 0x03, 0x12, 0x18, 0x0a, 0x14, 0x46,
+	0x61, 0x69, 0x6c, 0x65, 0x64, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x53, 0x65, 0x73, 0x73,
+	0x69, 0x6f, 0x6e, 0x10, 0x04, 0x42, 0x2a, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
+	0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x65, 0x64, 0x67,
+	0x65, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x5f, 0x63, 0x74, 0x72, 0x6c, 0x5f, 0x70,
+	0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/common/pb/edge_ctrl_pb/edge_ctrl.proto b/common/pb/edge_ctrl_pb/edge_ctrl.proto
index 58cbe9b77..0b971bcb5 100644
--- a/common/pb/edge_ctrl_pb/edge_ctrl.proto
+++ b/common/pb/edge_ctrl_pb/edge_ctrl.proto
@@ -244,6 +244,7 @@ enum CreateTerminatorResult {
   FailedIdConflict = 1;
   FailedOther = 2;
   FailedBusy = 3;
+  FailedInvalidSession = 4;
 }
 
 message CreateTerminatorV2Response {
diff --git a/common/pb/edge_mgmt_pb/edge_mgmt.pb.go b/common/pb/edge_mgmt_pb/edge_mgmt.pb.go
index ffd8fbb8c..b8fa212d6 100644
--- a/common/pb/edge_mgmt_pb/edge_mgmt.pb.go
+++ b/common/pb/edge_mgmt_pb/edge_mgmt.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v4.23.4
+// 	protoc        v3.21.12
 // source: edge_mgmt.proto
 
 package edge_mgmt_pb
diff --git a/common/pb/mgmt_pb/impl.go b/common/pb/mgmt_pb/impl.go
index 2f6e08885..4ae9e8299 100644
--- a/common/pb/mgmt_pb/impl.go
+++ b/common/pb/mgmt_pb/impl.go
@@ -35,3 +35,15 @@ func (request *ValidateRouterLinksResponse) GetContentType() int32 {
 func (request *RouterLinkDetails) GetContentType() int32 {
 	return int32(ContentType_ValidateRouterLinksResultType)
 }
+
+func (request *ValidateRouterSdkTerminatorsRequest) GetContentType() int32 {
+	return int32(ContentType_ValidateRouterSdkTerminatorsRequestType)
+}
+
+func (request *ValidateRouterSdkTerminatorsResponse) GetContentType() int32 {
+	return int32(ContentType_ValidateRouterSdkTerminatorsResponseType)
+}
+
+func (request *RouterSdkTerminatorsDetails) GetContentType() int32 {
+	return int32(ContentType_ValidateRouterSdkTerminatorsResultType)
+}
diff --git a/common/pb/mgmt_pb/mgmt.pb.go b/common/pb/mgmt_pb/mgmt.pb.go
index 76c9a8b29..28210486c 100644
--- a/common/pb/mgmt_pb/mgmt.pb.go
+++ b/common/pb/mgmt_pb/mgmt.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.31.0
-// 	protoc        v4.23.4
+// 	protoc        v3.21.12
 // source: mgmt.proto
 
 package mgmt_pb
@@ -56,12 +56,15 @@ const (
 	ContentType_RaftTransferLeadershipRequestType ContentType = 10084
 	ContentType_RaftInitFromDb                    ContentType = 10085
 	// Validate
-	ContentType_ValidateTerminatorsRequestType  ContentType = 10100
-	ContentType_ValidateTerminatorResponseType  ContentType = 10101
-	ContentType_ValidateTerminatorResultType    ContentType = 10102
-	ContentType_ValidateRouterLinksRequestType  ContentType = 10103
-	ContentType_ValidateRouterLinksResponseType ContentType = 10104
-	ContentType_ValidateRouterLinksResultType   ContentType = 10105
+	ContentType_ValidateTerminatorsRequestType           ContentType = 10100
+	ContentType_ValidateTerminatorResponseType           ContentType = 10101
+	ContentType_ValidateTerminatorResultType             ContentType = 10102
+	ContentType_ValidateRouterLinksRequestType           ContentType = 10103
+	ContentType_ValidateRouterLinksResponseType          ContentType = 10104
+	ContentType_ValidateRouterLinksResultType            ContentType = 10105
+	ContentType_ValidateRouterSdkTerminatorsRequestType  ContentType = 10106
+	ContentType_ValidateRouterSdkTerminatorsResponseType ContentType = 10107
+	ContentType_ValidateRouterSdkTerminatorsResultType   ContentType = 10108
 )
 
 // Enum value maps for ContentType.
@@ -98,6 +101,9 @@ var (
 		10103: "ValidateRouterLinksRequestType",
 		10104: "ValidateRouterLinksResponseType",
 		10105: "ValidateRouterLinksResultType",
+		10106: "ValidateRouterSdkTerminatorsRequestType",
+		10107: "ValidateRouterSdkTerminatorsResponseType",
+		10108: "ValidateRouterSdkTerminatorsResultType",
 	}
 	ContentType_value = map[string]int32{
 		"Zero":                                      0,
@@ -131,6 +137,9 @@ var (
 		"ValidateRouterLinksRequestType":            10103,
 		"ValidateRouterLinksResponseType":           10104,
 		"ValidateRouterLinksResultType":             10105,
+		"ValidateRouterSdkTerminatorsRequestType":   10106,
+		"ValidateRouterSdkTerminatorsResponseType":  10107,
+		"ValidateRouterSdkTerminatorsResultType":    10108,
 	}
 )
 
@@ -1637,6 +1646,290 @@ func (x *RouterLinkDetail) GetDialed() bool {
 	return false
 }
 
+type ValidateRouterSdkTerminatorsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Filter string `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"`
+}
+
+func (x *ValidateRouterSdkTerminatorsRequest) Reset() {
+	*x = ValidateRouterSdkTerminatorsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_mgmt_proto_msgTypes[17]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ValidateRouterSdkTerminatorsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ValidateRouterSdkTerminatorsRequest) ProtoMessage() {}
+
+func (x *ValidateRouterSdkTerminatorsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_mgmt_proto_msgTypes[17]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ValidateRouterSdkTerminatorsRequest.ProtoReflect.Descriptor instead.
+func (*ValidateRouterSdkTerminatorsRequest) Descriptor() ([]byte, []int) {
+	return file_mgmt_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *ValidateRouterSdkTerminatorsRequest) GetFilter() string {
+	if x != nil {
+		return x.Filter
+	}
+	return ""
+}
+
+type ValidateRouterSdkTerminatorsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Success     bool   `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+	Message     string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+	RouterCount uint64 `protobuf:"varint,3,opt,name=routerCount,proto3" json:"routerCount,omitempty"`
+}
+
+func (x *ValidateRouterSdkTerminatorsResponse) Reset() {
+	*x = ValidateRouterSdkTerminatorsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_mgmt_proto_msgTypes[18]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ValidateRouterSdkTerminatorsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ValidateRouterSdkTerminatorsResponse) ProtoMessage() {}
+
+func (x *ValidateRouterSdkTerminatorsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_mgmt_proto_msgTypes[18]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ValidateRouterSdkTerminatorsResponse.ProtoReflect.Descriptor instead.
+func (*ValidateRouterSdkTerminatorsResponse) Descriptor() ([]byte, []int) {
+	return file_mgmt_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *ValidateRouterSdkTerminatorsResponse) GetSuccess() bool {
+	if x != nil {
+		return x.Success
+	}
+	return false
+}
+
+func (x *ValidateRouterSdkTerminatorsResponse) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+func (x *ValidateRouterSdkTerminatorsResponse) GetRouterCount() uint64 {
+	if x != nil {
+		return x.RouterCount
+	}
+	return 0
+}
+
+type RouterSdkTerminatorsDetails struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	RouterId        string                       `protobuf:"bytes,1,opt,name=routerId,proto3" json:"routerId,omitempty"`
+	RouterName      string                       `protobuf:"bytes,2,opt,name=routerName,proto3" json:"routerName,omitempty"`
+	ValidateSuccess bool                         `protobuf:"varint,3,opt,name=validateSuccess,proto3" json:"validateSuccess,omitempty"`
+	Message         string                       `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"`
+	Details         []*RouterSdkTerminatorDetail `protobuf:"bytes,5,rep,name=details,proto3" json:"details,omitempty"`
+}
+
+func (x *RouterSdkTerminatorsDetails) Reset() {
+	*x = RouterSdkTerminatorsDetails{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_mgmt_proto_msgTypes[19]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RouterSdkTerminatorsDetails) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RouterSdkTerminatorsDetails) ProtoMessage() {}
+
+func (x *RouterSdkTerminatorsDetails) ProtoReflect() protoreflect.Message {
+	mi := &file_mgmt_proto_msgTypes[19]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RouterSdkTerminatorsDetails.ProtoReflect.Descriptor instead.
+func (*RouterSdkTerminatorsDetails) Descriptor() ([]byte, []int) {
+	return file_mgmt_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *RouterSdkTerminatorsDetails) GetRouterId() string {
+	if x != nil {
+		return x.RouterId
+	}
+	return ""
+}
+
+func (x *RouterSdkTerminatorsDetails) GetRouterName() string {
+	if x != nil {
+		return x.RouterName
+	}
+	return ""
+}
+
+func (x *RouterSdkTerminatorsDetails) GetValidateSuccess() bool {
+	if x != nil {
+		return x.ValidateSuccess
+	}
+	return false
+}
+
+func (x *RouterSdkTerminatorsDetails) GetMessage() string {
+	if x != nil {
+		return x.Message
+	}
+	return ""
+}
+
+func (x *RouterSdkTerminatorsDetails) GetDetails() []*RouterSdkTerminatorDetail {
+	if x != nil {
+		return x.Details
+	}
+	return nil
+}
+
+type RouterSdkTerminatorDetail struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	TerminatorId     string          `protobuf:"bytes,1,opt,name=terminatorId,proto3" json:"terminatorId,omitempty"`
+	CtrlState        TerminatorState `protobuf:"varint,2,opt,name=ctrlState,proto3,enum=ziti.mgmt_pb.TerminatorState" json:"ctrlState,omitempty"`
+	RouterState      string          `protobuf:"bytes,3,opt,name=routerState,proto3" json:"routerState,omitempty"`
+	IsValid          bool            `protobuf:"varint,4,opt,name=isValid,proto3" json:"isValid,omitempty"`
+	OperaationActive bool            `protobuf:"varint,5,opt,name=operaationActive,proto3" json:"operaationActive,omitempty"`
+	CreateTime       string          `protobuf:"bytes,6,opt,name=createTime,proto3" json:"createTime,omitempty"`
+	LastAttempt      string          `protobuf:"bytes,7,opt,name=lastAttempt,proto3" json:"lastAttempt,omitempty"`
+}
+
+func (x *RouterSdkTerminatorDetail) Reset() {
+	*x = RouterSdkTerminatorDetail{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_mgmt_proto_msgTypes[20]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RouterSdkTerminatorDetail) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RouterSdkTerminatorDetail) ProtoMessage() {}
+
+func (x *RouterSdkTerminatorDetail) ProtoReflect() protoreflect.Message {
+	mi := &file_mgmt_proto_msgTypes[20]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RouterSdkTerminatorDetail.ProtoReflect.Descriptor instead.
+func (*RouterSdkTerminatorDetail) Descriptor() ([]byte, []int) {
+	return file_mgmt_proto_rawDescGZIP(), []int{20}
+}
+
+func (x *RouterSdkTerminatorDetail) GetTerminatorId() string {
+	if x != nil {
+		return x.TerminatorId
+	}
+	return ""
+}
+
+func (x *RouterSdkTerminatorDetail) GetCtrlState() TerminatorState {
+	if x != nil {
+		return x.CtrlState
+	}
+	return TerminatorState_Valid
+}
+
+func (x *RouterSdkTerminatorDetail) GetRouterState() string {
+	if x != nil {
+		return x.RouterState
+	}
+	return ""
+}
+
+func (x *RouterSdkTerminatorDetail) GetIsValid() bool {
+	if x != nil {
+		return x.IsValid
+	}
+	return false
+}
+
+func (x *RouterSdkTerminatorDetail) GetOperaationActive() bool {
+	if x != nil {
+		return x.OperaationActive
+	}
+	return false
+}
+
+func (x *RouterSdkTerminatorDetail) GetCreateTime() string {
+	if x != nil {
+		return x.CreateTime
+	}
+	return ""
+}
+
+func (x *RouterSdkTerminatorDetail) GetLastAttempt() string {
+	if x != nil {
+		return x.LastAttempt
+	}
+	return ""
+}
+
 type StreamMetricsRequest_MetricMatcher struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -1649,7 +1942,7 @@ type StreamMetricsRequest_MetricMatcher struct {
 func (x *StreamMetricsRequest_MetricMatcher) Reset() {
 	*x = StreamMetricsRequest_MetricMatcher{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_mgmt_proto_msgTypes[17]
+		mi := &file_mgmt_proto_msgTypes[21]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1662,7 +1955,7 @@ func (x *StreamMetricsRequest_MetricMatcher) String() string {
 func (*StreamMetricsRequest_MetricMatcher) ProtoMessage() {}
 
 func (x *StreamMetricsRequest_MetricMatcher) ProtoReflect() protoreflect.Message {
-	mi := &file_mgmt_proto_msgTypes[17]
+	mi := &file_mgmt_proto_msgTypes[21]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1706,7 +1999,7 @@ type StreamMetricsEvent_IntervalMetric struct {
 func (x *StreamMetricsEvent_IntervalMetric) Reset() {
 	*x = StreamMetricsEvent_IntervalMetric{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_mgmt_proto_msgTypes[21]
+		mi := &file_mgmt_proto_msgTypes[25]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1719,7 +2012,7 @@ func (x *StreamMetricsEvent_IntervalMetric) String() string {
 func (*StreamMetricsEvent_IntervalMetric) ProtoMessage() {}
 
 func (x *StreamMetricsEvent_IntervalMetric) ProtoReflect() protoreflect.Message {
-	mi := &file_mgmt_proto_msgTypes[21]
+	mi := &file_mgmt_proto_msgTypes[25]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1776,7 +2069,7 @@ type InspectResponse_InspectValue struct {
 func (x *InspectResponse_InspectValue) Reset() {
 	*x = InspectResponse_InspectValue{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_mgmt_proto_msgTypes[24]
+		mi := &file_mgmt_proto_msgTypes[28]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1789,7 +2082,7 @@ func (x *InspectResponse_InspectValue) String() string {
 func (*InspectResponse_InspectValue) ProtoMessage() {}
 
 func (x *InspectResponse_InspectValue) ProtoReflect() protoreflect.Message {
-	mi := &file_mgmt_proto_msgTypes[24]
+	mi := &file_mgmt_proto_msgTypes[28]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2071,104 +2364,157 @@ var file_mgmt_proto_rawDesc = []byte{
 	0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64,
 	0x65, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64,
 	0x69, 0x61, 0x6c, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x69, 0x61,
-	0x6c, 0x65, 0x64, 0x2a, 0x95, 0x08, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54,
-	0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x1c, 0x0a,
-	0x17, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71,
-	0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb8, 0x4e, 0x12, 0x1a, 0x0a, 0x15, 0x53,
-	0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74,
-	0x54, 0x79, 0x70, 0x65, 0x10, 0xb9, 0x4e, 0x12, 0x20, 0x0a, 0x1b, 0x54, 0x6f, 0x67, 0x67, 0x6c,
-	0x65, 0x50, 0x69, 0x70, 0x65, 0x54, 0x72, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbc, 0x4e, 0x12, 0x23, 0x0a, 0x1e, 0x54, 0x6f, 0x67,
-	0x67, 0x6c, 0x65, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x54, 0x72, 0x61, 0x63, 0x65, 0x73,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbd, 0x4e, 0x12, 0x1c,
-	0x0a, 0x17, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x72, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbe, 0x4e, 0x12, 0x1a, 0x0a, 0x15,
-	0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x72, 0x61, 0x63, 0x65, 0x73, 0x45, 0x76, 0x65, 0x6e,
-	0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbf, 0x4e, 0x12, 0x17, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70,
-	0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xc0,
-	0x4e, 0x12, 0x18, 0x0a, 0x13, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xc1, 0x4e, 0x12, 0x1a, 0x0a, 0x15, 0x53,
-	0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x44, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x54, 0x79, 0x70, 0x65, 0x10, 0xd6, 0x4e, 0x12, 0x25, 0x0a, 0x20, 0x52, 0x6f, 0x75, 0x74, 0x65,
-	0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x46, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x6b,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xd7, 0x4e, 0x12, 0x2c,
-	0x0a, 0x27, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x6f, 0x67,
-	0x67, 0x6c, 0x65, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xd8, 0x4e, 0x12, 0x26, 0x0a, 0x21,
-	0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74,
-	0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70,
-	0x65, 0x10, 0xd9, 0x4e, 0x12, 0x2e, 0x0a, 0x29, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65,
-	0x62, 0x75, 0x67, 0x44, 0x75, 0x6d, 0x70, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72,
-	0x54, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70,
-	0x65, 0x10, 0xda, 0x4e, 0x12, 0x24, 0x0a, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65,
-	0x62, 0x75, 0x67, 0x44, 0x75, 0x6d, 0x70, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xdb, 0x4e, 0x12, 0x22, 0x0a, 0x1d, 0x52, 0x6f,
-	0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x55, 0x6e, 0x72, 0x6f, 0x75, 0x74, 0x65,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xdc, 0x4e, 0x12, 0x1d,
-	0x0a, 0x18, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x51, 0x75, 0x69, 0x65, 0x73, 0x63, 0x65, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xdd, 0x4e, 0x12, 0x1f, 0x0a,
-	0x1a, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x71, 0x75, 0x69, 0x65, 0x73, 0x63, 0x65,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xde, 0x4e, 0x12, 0x22,
-	0x0a, 0x1d, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x73,
-	0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10,
-	0xdf, 0x4e, 0x12, 0x1f, 0x0a, 0x1a, 0x52, 0x61, 0x66, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65,
-	0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65,
-	0x10, 0xe0, 0x4e, 0x12, 0x20, 0x0a, 0x1b, 0x52, 0x61, 0x66, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x4d,
-	0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79,
-	0x70, 0x65, 0x10, 0xe1, 0x4e, 0x12, 0x1b, 0x0a, 0x16, 0x52, 0x61, 0x66, 0x74, 0x41, 0x64, 0x64,
-	0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10,
-	0xe2, 0x4e, 0x12, 0x1e, 0x0a, 0x19, 0x52, 0x61, 0x66, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65,
-	0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10,
-	0xe3, 0x4e, 0x12, 0x26, 0x0a, 0x21, 0x52, 0x61, 0x66, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66,
-	0x65, 0x72, 0x4c, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe4, 0x4e, 0x12, 0x13, 0x0a, 0x0e, 0x52, 0x61,
-	0x66, 0x74, 0x49, 0x6e, 0x69, 0x74, 0x46, 0x72, 0x6f, 0x6d, 0x44, 0x62, 0x10, 0xe5, 0x4e, 0x12,
-	0x23, 0x0a, 0x1e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69,
-	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70,
-	0x65, 0x10, 0xf4, 0x4e, 0x12, 0x23, 0x0a, 0x1e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65,
-	0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf5, 0x4e, 0x12, 0x21, 0x0a, 0x1c, 0x56, 0x61, 0x6c,
-	0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52,
-	0x65, 0x73, 0x75, 0x6c, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf6, 0x4e, 0x12, 0x23, 0x0a, 0x1e,
-	0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x4c, 0x69,
-	0x6e, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf7,
-	0x4e, 0x12, 0x24, 0x0a, 0x1f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75,
-	0x74, 0x65, 0x72, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
-	0x54, 0x79, 0x70, 0x65, 0x10, 0xf8, 0x4e, 0x12, 0x22, 0x0a, 0x1d, 0x56, 0x61, 0x6c, 0x69, 0x64,
-	0x61, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x52, 0x65,
-	0x73, 0x75, 0x6c, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf9, 0x4e, 0x2a, 0x53, 0x0a, 0x06, 0x48,
-	0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x6f, 0x6e, 0x65, 0x48, 0x65, 0x61,
-	0x64, 0x65, 0x72, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79,
-	0x70, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0x0a, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x74,
-	0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x10, 0x0b, 0x12, 0x10,
-	0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x49, 0x64, 0x10, 0x0c,
-	0x2a, 0x78, 0x0a, 0x16, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69,
-	0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x69,
-	0x72, 0x63, 0x75, 0x69, 0x74, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x10, 0x00, 0x12, 0x12,
-	0x0a, 0x0e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64,
-	0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x50, 0x72, 0x65,
-	0x73, 0x65, 0x6e, 0x74, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x61, 0x74, 0x68, 0x55, 0x70,
-	0x64, 0x61, 0x74, 0x65, 0x64, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x69, 0x72, 0x63, 0x75,
-	0x69, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x04, 0x2a, 0x2b, 0x0a, 0x0f, 0x54, 0x72,
-	0x61, 0x63, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a,
-	0x07, 0x45, 0x58, 0x43, 0x4c, 0x55, 0x44, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e,
-	0x43, 0x4c, 0x55, 0x44, 0x45, 0x10, 0x01, 0x2a, 0x77, 0x0a, 0x0f, 0x54, 0x65, 0x72, 0x6d, 0x69,
-	0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x56, 0x61,
-	0x6c, 0x69, 0x64, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e,
-	0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x6b,
-	0x6e, 0x6f, 0x77, 0x6e, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x10, 0x02, 0x12, 0x1c, 0x0a,
-	0x18, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x54,
-	0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x49,
-	0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x61, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x10, 0x04,
-	0x2a, 0x53, 0x0a, 0x09, 0x4c, 0x69, 0x6e, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0f, 0x0a,
-	0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x13,
-	0x0a, 0x0f, 0x4c, 0x69, 0x6e, 0x6b, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65,
-	0x64, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x50, 0x65, 0x6e, 0x64, 0x69,
-	0x6e, 0x67, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x44, 0x69, 0x61, 0x6c,
-	0x69, 0x6e, 0x67, 0x10, 0x03, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
-	0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f, 0x66, 0x61, 0x62,
-	0x72, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x67, 0x6d, 0x74, 0x5f, 0x70, 0x62, 0x62, 0x06,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6c, 0x65, 0x64, 0x22, 0x3d, 0x0a, 0x23, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52,
+	0x6f, 0x75, 0x74, 0x65, 0x72, 0x53, 0x64, 0x6b, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
+	0x6f, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x69,
+	0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74,
+	0x65, 0x72, 0x22, 0x7c, 0x0a, 0x24, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f,
+	0x75, 0x74, 0x65, 0x72, 0x53, 0x64, 0x6b, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75,
+	0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63,
+	0x63, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20,
+	0x0a, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20,
+	0x01, 0x28, 0x04, 0x52, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74,
+	0x22, 0xe0, 0x01, 0x0a, 0x1b, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x53, 0x64, 0x6b, 0x54, 0x65,
+	0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73,
+	0x12, 0x1a, 0x0a, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a,
+	0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f,
+	0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53,
+	0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
+	0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+	0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x27, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e, 0x6d, 0x67, 0x6d, 0x74, 0x5f, 0x70, 0x62,
+	0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x53, 0x64, 0x6b, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e,
+	0x61, 0x74, 0x6f, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61,
+	0x69, 0x6c, 0x73, 0x22, 0xa6, 0x02, 0x0a, 0x19, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x53, 0x64,
+	0x6b, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x44, 0x65, 0x74, 0x61, 0x69,
+	0x6c, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x49,
+	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
+	0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x09, 0x63, 0x74, 0x72, 0x6c, 0x53, 0x74, 0x61,
+	0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x7a, 0x69, 0x74, 0x69, 0x2e,
+	0x6d, 0x67, 0x6d, 0x74, 0x5f, 0x70, 0x62, 0x2e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74,
+	0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x09, 0x63, 0x74, 0x72, 0x6c, 0x53, 0x74, 0x61,
+	0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
+	0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x53,
+	0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18,
+	0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x2a,
+	0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69,
+	0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x61,
+	0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x72,
+	0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
+	0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6c, 0x61,
+	0x73, 0x74, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x0b, 0x6c, 0x61, 0x73, 0x74, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x2a, 0x9f, 0x09, 0x0a,
+	0x0b, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04,
+	0x5a, 0x65, 0x72, 0x6f, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x17, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
+	0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70,
+	0x65, 0x10, 0xb8, 0x4e, 0x12, 0x1a, 0x0a, 0x15, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x76,
+	0x65, 0x6e, 0x74, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xb9, 0x4e,
+	0x12, 0x20, 0x0a, 0x1b, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x50, 0x69, 0x70, 0x65, 0x54, 0x72,
+	0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10,
+	0xbc, 0x4e, 0x12, 0x23, 0x0a, 0x1e, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x43, 0x69, 0x72, 0x63,
+	0x75, 0x69, 0x74, 0x54, 0x72, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x54, 0x79, 0x70, 0x65, 0x10, 0xbd, 0x4e, 0x12, 0x1c, 0x0a, 0x17, 0x53, 0x74, 0x72, 0x65, 0x61,
+	0x6d, 0x54, 0x72, 0x61, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79,
+	0x70, 0x65, 0x10, 0xbe, 0x4e, 0x12, 0x1a, 0x0a, 0x15, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54,
+	0x72, 0x61, 0x63, 0x65, 0x73, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xbf,
+	0x4e, 0x12, 0x17, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xc0, 0x4e, 0x12, 0x18, 0x0a, 0x13, 0x49, 0x6e,
+	0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70,
+	0x65, 0x10, 0xc1, 0x4e, 0x12, 0x1a, 0x0a, 0x15, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74,
+	0x44, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xd6, 0x4e,
+	0x12, 0x25, 0x0a, 0x20, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x46,
+	0x6f, 0x72, 0x67, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x54, 0x79, 0x70, 0x65, 0x10, 0xd7, 0x4e, 0x12, 0x2c, 0x0a, 0x27, 0x52, 0x6f, 0x75, 0x74, 0x65,
+	0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x43, 0x74, 0x72, 0x6c,
+	0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79,
+	0x70, 0x65, 0x10, 0xd8, 0x4e, 0x12, 0x26, 0x0a, 0x21, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44,
+	0x65, 0x62, 0x75, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xd9, 0x4e, 0x12, 0x2e, 0x0a,
+	0x29, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x44, 0x75, 0x6d, 0x70,
+	0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x65, 0x72, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xda, 0x4e, 0x12, 0x24, 0x0a,
+	0x1f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x62, 0x75, 0x67, 0x44, 0x75, 0x6d, 0x70,
+	0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65,
+	0x10, 0xdb, 0x4e, 0x12, 0x22, 0x0a, 0x1d, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x44, 0x65, 0x62,
+	0x75, 0x67, 0x55, 0x6e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x54, 0x79, 0x70, 0x65, 0x10, 0xdc, 0x4e, 0x12, 0x1d, 0x0a, 0x18, 0x52, 0x6f, 0x75, 0x74, 0x65,
+	0x72, 0x51, 0x75, 0x69, 0x65, 0x73, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54,
+	0x79, 0x70, 0x65, 0x10, 0xdd, 0x4e, 0x12, 0x1f, 0x0a, 0x1a, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72,
+	0x44, 0x65, 0x71, 0x75, 0x69, 0x65, 0x73, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x54, 0x79, 0x70, 0x65, 0x10, 0xde, 0x4e, 0x12, 0x22, 0x0a, 0x1d, 0x52, 0x6f, 0x75, 0x74, 0x65,
+	0x72, 0x44, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xdf, 0x4e, 0x12, 0x1f, 0x0a, 0x1a, 0x52,
+	0x61, 0x66, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe0, 0x4e, 0x12, 0x20, 0x0a, 0x1b,
+	0x52, 0x61, 0x66, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe1, 0x4e, 0x12, 0x1b,
+	0x0a, 0x16, 0x52, 0x61, 0x66, 0x74, 0x41, 0x64, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe2, 0x4e, 0x12, 0x1e, 0x0a, 0x19, 0x52,
+	0x61, 0x66, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xe3, 0x4e, 0x12, 0x26, 0x0a, 0x21, 0x52,
+	0x61, 0x66, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x4c, 0x65, 0x61, 0x64, 0x65,
+	0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65,
+	0x10, 0xe4, 0x4e, 0x12, 0x13, 0x0a, 0x0e, 0x52, 0x61, 0x66, 0x74, 0x49, 0x6e, 0x69, 0x74, 0x46,
+	0x72, 0x6f, 0x6d, 0x44, 0x62, 0x10, 0xe5, 0x4e, 0x12, 0x23, 0x0a, 0x1e, 0x56, 0x61, 0x6c, 0x69,
+	0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x73, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf4, 0x4e, 0x12, 0x23, 0x0a,
+	0x1e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
+	0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10,
+	0xf5, 0x4e, 0x12, 0x21, 0x0a, 0x1c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x65,
+	0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x54, 0x79,
+	0x70, 0x65, 0x10, 0xf6, 0x4e, 0x12, 0x23, 0x0a, 0x1e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74,
+	0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf7, 0x4e, 0x12, 0x24, 0x0a, 0x1f, 0x56, 0x61,
+	0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x4c, 0x69, 0x6e, 0x6b,
+	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xf8, 0x4e,
+	0x12, 0x22, 0x0a, 0x1d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75, 0x74,
+	0x65, 0x72, 0x4c, 0x69, 0x6e, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x54, 0x79, 0x70,
+	0x65, 0x10, 0xf9, 0x4e, 0x12, 0x2c, 0x0a, 0x27, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65,
+	0x52, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x53, 0x64, 0x6b, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61,
+	0x74, 0x6f, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10,
+	0xfa, 0x4e, 0x12, 0x2d, 0x0a, 0x28, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f,
+	0x75, 0x74, 0x65, 0x72, 0x53, 0x64, 0x6b, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f,
+	0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x10, 0xfb,
+	0x4e, 0x12, 0x2b, 0x0a, 0x26, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x75,
+	0x74, 0x65, 0x72, 0x53, 0x64, 0x6b, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72,
+	0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x54, 0x79, 0x70, 0x65, 0x10, 0xfc, 0x4e, 0x2a, 0x53,
+	0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x6f, 0x6e, 0x65,
+	0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x76, 0x65, 0x6e,
+	0x74, 0x54, 0x79, 0x70, 0x65, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x10, 0x0a, 0x12, 0x12, 0x0a,
+	0x0e, 0x43, 0x74, 0x72, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x54, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x10,
+	0x0b, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x49,
+	0x64, 0x10, 0x0c, 0x2a, 0x78, 0x0a, 0x16, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x69, 0x72,
+	0x63, 0x75, 0x69, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
+	0x0e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x10,
+	0x00, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x44, 0x65, 0x6c, 0x65,
+	0x74, 0x65, 0x64, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74,
+	0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x61, 0x74,
+	0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x69,
+	0x72, 0x63, 0x75, 0x69, 0x74, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x04, 0x2a, 0x2b, 0x0a,
+	0x0f, 0x54, 0x72, 0x61, 0x63, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65,
+	0x12, 0x0b, 0x0a, 0x07, 0x45, 0x58, 0x43, 0x4c, 0x55, 0x44, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a,
+	0x07, 0x49, 0x4e, 0x43, 0x4c, 0x55, 0x44, 0x45, 0x10, 0x01, 0x2a, 0x77, 0x0a, 0x0f, 0x54, 0x65,
+	0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x09, 0x0a,
+	0x05, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, 0x6e,
+	0x6f, 0x77, 0x6e, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64,
+	0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x10, 0x02,
+	0x12, 0x1c, 0x0a, 0x18, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x6b, 0x6e, 0x6f,
+	0x77, 0x6e, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x10, 0x03, 0x12, 0x13,
+	0x0a, 0x0f, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x61, 0x64, 0x53, 0x74, 0x61, 0x74,
+	0x65, 0x10, 0x04, 0x2a, 0x53, 0x0a, 0x09, 0x4c, 0x69, 0x6e, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x65,
+	0x12, 0x0f, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10,
+	0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4c, 0x69, 0x6e, 0x6b, 0x45, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x69,
+	0x73, 0x68, 0x65, 0x64, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x50, 0x65,
+	0x6e, 0x64, 0x69, 0x6e, 0x67, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x44,
+	0x69, 0x61, 0x6c, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68,
+	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x7a, 0x69, 0x74, 0x69, 0x2f,
+	0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x67, 0x6d, 0x74, 0x5f, 0x70,
+	0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -2184,66 +2530,72 @@ func file_mgmt_proto_rawDescGZIP() []byte {
 }
 
 var file_mgmt_proto_enumTypes = make([]protoimpl.EnumInfo, 6)
-var file_mgmt_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
+var file_mgmt_proto_msgTypes = make([]protoimpl.MessageInfo, 29)
 var file_mgmt_proto_goTypes = []interface{}{
-	(ContentType)(0),                           // 0: ziti.mgmt_pb.ContentType
-	(Header)(0),                                // 1: ziti.mgmt_pb.Header
-	(StreamCircuitEventType)(0),                // 2: ziti.mgmt_pb.StreamCircuitEventType
-	(TraceFilterType)(0),                       // 3: ziti.mgmt_pb.TraceFilterType
-	(TerminatorState)(0),                       // 4: ziti.mgmt_pb.TerminatorState
-	(LinkState)(0),                             // 5: ziti.mgmt_pb.LinkState
-	(*StreamMetricsRequest)(nil),               // 6: ziti.mgmt_pb.StreamMetricsRequest
-	(*StreamMetricsEvent)(nil),                 // 7: ziti.mgmt_pb.StreamMetricsEvent
-	(*Path)(nil),                               // 8: ziti.mgmt_pb.Path
-	(*StreamCircuitsEvent)(nil),                // 9: ziti.mgmt_pb.StreamCircuitsEvent
-	(*ToggleCircuitTracesRequest)(nil),         // 10: ziti.mgmt_pb.ToggleCircuitTracesRequest
-	(*StreamTracesRequest)(nil),                // 11: ziti.mgmt_pb.StreamTracesRequest
-	(*InspectRequest)(nil),                     // 12: ziti.mgmt_pb.InspectRequest
-	(*InspectResponse)(nil),                    // 13: ziti.mgmt_pb.InspectResponse
-	(*RaftMember)(nil),                         // 14: ziti.mgmt_pb.RaftMember
-	(*RaftMemberListResponse)(nil),             // 15: ziti.mgmt_pb.RaftMemberListResponse
-	(*ValidateTerminatorsRequest)(nil),         // 16: ziti.mgmt_pb.ValidateTerminatorsRequest
-	(*ValidateTerminatorsResponse)(nil),        // 17: ziti.mgmt_pb.ValidateTerminatorsResponse
-	(*TerminatorDetail)(nil),                   // 18: ziti.mgmt_pb.TerminatorDetail
-	(*ValidateRouterLinksRequest)(nil),         // 19: ziti.mgmt_pb.ValidateRouterLinksRequest
-	(*ValidateRouterLinksResponse)(nil),        // 20: ziti.mgmt_pb.ValidateRouterLinksResponse
-	(*RouterLinkDetails)(nil),                  // 21: ziti.mgmt_pb.RouterLinkDetails
-	(*RouterLinkDetail)(nil),                   // 22: ziti.mgmt_pb.RouterLinkDetail
-	(*StreamMetricsRequest_MetricMatcher)(nil), // 23: ziti.mgmt_pb.StreamMetricsRequest.MetricMatcher
-	nil, // 24: ziti.mgmt_pb.StreamMetricsEvent.TagsEntry
-	nil, // 25: ziti.mgmt_pb.StreamMetricsEvent.IntMetricsEntry
-	nil, // 26: ziti.mgmt_pb.StreamMetricsEvent.FloatMetricsEntry
-	(*StreamMetricsEvent_IntervalMetric)(nil), // 27: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric
-	nil,                                  // 28: ziti.mgmt_pb.StreamMetricsEvent.MetricGroupEntry
-	nil,                                  // 29: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.ValuesEntry
-	(*InspectResponse_InspectValue)(nil), // 30: ziti.mgmt_pb.InspectResponse.InspectValue
-	(*timestamppb.Timestamp)(nil),        // 31: google.protobuf.Timestamp
+	(ContentType)(0),                             // 0: ziti.mgmt_pb.ContentType
+	(Header)(0),                                  // 1: ziti.mgmt_pb.Header
+	(StreamCircuitEventType)(0),                  // 2: ziti.mgmt_pb.StreamCircuitEventType
+	(TraceFilterType)(0),                         // 3: ziti.mgmt_pb.TraceFilterType
+	(TerminatorState)(0),                         // 4: ziti.mgmt_pb.TerminatorState
+	(LinkState)(0),                               // 5: ziti.mgmt_pb.LinkState
+	(*StreamMetricsRequest)(nil),                 // 6: ziti.mgmt_pb.StreamMetricsRequest
+	(*StreamMetricsEvent)(nil),                   // 7: ziti.mgmt_pb.StreamMetricsEvent
+	(*Path)(nil),                                 // 8: ziti.mgmt_pb.Path
+	(*StreamCircuitsEvent)(nil),                  // 9: ziti.mgmt_pb.StreamCircuitsEvent
+	(*ToggleCircuitTracesRequest)(nil),           // 10: ziti.mgmt_pb.ToggleCircuitTracesRequest
+	(*StreamTracesRequest)(nil),                  // 11: ziti.mgmt_pb.StreamTracesRequest
+	(*InspectRequest)(nil),                       // 12: ziti.mgmt_pb.InspectRequest
+	(*InspectResponse)(nil),                      // 13: ziti.mgmt_pb.InspectResponse
+	(*RaftMember)(nil),                           // 14: ziti.mgmt_pb.RaftMember
+	(*RaftMemberListResponse)(nil),               // 15: ziti.mgmt_pb.RaftMemberListResponse
+	(*ValidateTerminatorsRequest)(nil),           // 16: ziti.mgmt_pb.ValidateTerminatorsRequest
+	(*ValidateTerminatorsResponse)(nil),          // 17: ziti.mgmt_pb.ValidateTerminatorsResponse
+	(*TerminatorDetail)(nil),                     // 18: ziti.mgmt_pb.TerminatorDetail
+	(*ValidateRouterLinksRequest)(nil),           // 19: ziti.mgmt_pb.ValidateRouterLinksRequest
+	(*ValidateRouterLinksResponse)(nil),          // 20: ziti.mgmt_pb.ValidateRouterLinksResponse
+	(*RouterLinkDetails)(nil),                    // 21: ziti.mgmt_pb.RouterLinkDetails
+	(*RouterLinkDetail)(nil),                     // 22: ziti.mgmt_pb.RouterLinkDetail
+	(*ValidateRouterSdkTerminatorsRequest)(nil),  // 23: ziti.mgmt_pb.ValidateRouterSdkTerminatorsRequest
+	(*ValidateRouterSdkTerminatorsResponse)(nil), // 24: ziti.mgmt_pb.ValidateRouterSdkTerminatorsResponse
+	(*RouterSdkTerminatorsDetails)(nil),          // 25: ziti.mgmt_pb.RouterSdkTerminatorsDetails
+	(*RouterSdkTerminatorDetail)(nil),            // 26: ziti.mgmt_pb.RouterSdkTerminatorDetail
+	(*StreamMetricsRequest_MetricMatcher)(nil),   // 27: ziti.mgmt_pb.StreamMetricsRequest.MetricMatcher
+	nil, // 28: ziti.mgmt_pb.StreamMetricsEvent.TagsEntry
+	nil, // 29: ziti.mgmt_pb.StreamMetricsEvent.IntMetricsEntry
+	nil, // 30: ziti.mgmt_pb.StreamMetricsEvent.FloatMetricsEntry
+	(*StreamMetricsEvent_IntervalMetric)(nil), // 31: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric
+	nil,                                  // 32: ziti.mgmt_pb.StreamMetricsEvent.MetricGroupEntry
+	nil,                                  // 33: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.ValuesEntry
+	(*InspectResponse_InspectValue)(nil), // 34: ziti.mgmt_pb.InspectResponse.InspectValue
+	(*timestamppb.Timestamp)(nil),        // 35: google.protobuf.Timestamp
 }
 var file_mgmt_proto_depIdxs = []int32{
-	23, // 0: ziti.mgmt_pb.StreamMetricsRequest.matchers:type_name -> ziti.mgmt_pb.StreamMetricsRequest.MetricMatcher
-	31, // 1: ziti.mgmt_pb.StreamMetricsEvent.timestamp:type_name -> google.protobuf.Timestamp
-	24, // 2: ziti.mgmt_pb.StreamMetricsEvent.tags:type_name -> ziti.mgmt_pb.StreamMetricsEvent.TagsEntry
-	25, // 3: ziti.mgmt_pb.StreamMetricsEvent.intMetrics:type_name -> ziti.mgmt_pb.StreamMetricsEvent.IntMetricsEntry
-	26, // 4: ziti.mgmt_pb.StreamMetricsEvent.floatMetrics:type_name -> ziti.mgmt_pb.StreamMetricsEvent.FloatMetricsEntry
-	27, // 5: ziti.mgmt_pb.StreamMetricsEvent.intervalMetrics:type_name -> ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric
-	28, // 6: ziti.mgmt_pb.StreamMetricsEvent.metricGroup:type_name -> ziti.mgmt_pb.StreamMetricsEvent.MetricGroupEntry
+	27, // 0: ziti.mgmt_pb.StreamMetricsRequest.matchers:type_name -> ziti.mgmt_pb.StreamMetricsRequest.MetricMatcher
+	35, // 1: ziti.mgmt_pb.StreamMetricsEvent.timestamp:type_name -> google.protobuf.Timestamp
+	28, // 2: ziti.mgmt_pb.StreamMetricsEvent.tags:type_name -> ziti.mgmt_pb.StreamMetricsEvent.TagsEntry
+	29, // 3: ziti.mgmt_pb.StreamMetricsEvent.intMetrics:type_name -> ziti.mgmt_pb.StreamMetricsEvent.IntMetricsEntry
+	30, // 4: ziti.mgmt_pb.StreamMetricsEvent.floatMetrics:type_name -> ziti.mgmt_pb.StreamMetricsEvent.FloatMetricsEntry
+	31, // 5: ziti.mgmt_pb.StreamMetricsEvent.intervalMetrics:type_name -> ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric
+	32, // 6: ziti.mgmt_pb.StreamMetricsEvent.metricGroup:type_name -> ziti.mgmt_pb.StreamMetricsEvent.MetricGroupEntry
 	2,  // 7: ziti.mgmt_pb.StreamCircuitsEvent.eventType:type_name -> ziti.mgmt_pb.StreamCircuitEventType
 	8,  // 8: ziti.mgmt_pb.StreamCircuitsEvent.path:type_name -> ziti.mgmt_pb.Path
 	3,  // 9: ziti.mgmt_pb.StreamTracesRequest.filterType:type_name -> ziti.mgmt_pb.TraceFilterType
-	30, // 10: ziti.mgmt_pb.InspectResponse.values:type_name -> ziti.mgmt_pb.InspectResponse.InspectValue
+	34, // 10: ziti.mgmt_pb.InspectResponse.values:type_name -> ziti.mgmt_pb.InspectResponse.InspectValue
 	14, // 11: ziti.mgmt_pb.RaftMemberListResponse.members:type_name -> ziti.mgmt_pb.RaftMember
 	4,  // 12: ziti.mgmt_pb.TerminatorDetail.state:type_name -> ziti.mgmt_pb.TerminatorState
 	22, // 13: ziti.mgmt_pb.RouterLinkDetails.linkDetails:type_name -> ziti.mgmt_pb.RouterLinkDetail
 	5,  // 14: ziti.mgmt_pb.RouterLinkDetail.ctrlState:type_name -> ziti.mgmt_pb.LinkState
 	5,  // 15: ziti.mgmt_pb.RouterLinkDetail.routerState:type_name -> ziti.mgmt_pb.LinkState
-	31, // 16: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.intervalStartUTC:type_name -> google.protobuf.Timestamp
-	31, // 17: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.intervalEndUTC:type_name -> google.protobuf.Timestamp
-	29, // 18: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.values:type_name -> ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.ValuesEntry
-	19, // [19:19] is the sub-list for method output_type
-	19, // [19:19] is the sub-list for method input_type
-	19, // [19:19] is the sub-list for extension type_name
-	19, // [19:19] is the sub-list for extension extendee
-	0,  // [0:19] is the sub-list for field type_name
+	26, // 16: ziti.mgmt_pb.RouterSdkTerminatorsDetails.details:type_name -> ziti.mgmt_pb.RouterSdkTerminatorDetail
+	4,  // 17: ziti.mgmt_pb.RouterSdkTerminatorDetail.ctrlState:type_name -> ziti.mgmt_pb.TerminatorState
+	35, // 18: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.intervalStartUTC:type_name -> google.protobuf.Timestamp
+	35, // 19: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.intervalEndUTC:type_name -> google.protobuf.Timestamp
+	33, // 20: ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.values:type_name -> ziti.mgmt_pb.StreamMetricsEvent.IntervalMetric.ValuesEntry
+	21, // [21:21] is the sub-list for method output_type
+	21, // [21:21] is the sub-list for method input_type
+	21, // [21:21] is the sub-list for extension type_name
+	21, // [21:21] is the sub-list for extension extendee
+	0,  // [0:21] is the sub-list for field type_name
 }
 
 func init() { file_mgmt_proto_init() }
@@ -2457,7 +2809,43 @@ func file_mgmt_proto_init() {
 			}
 		}
 		file_mgmt_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*StreamMetricsRequest_MetricMatcher); i {
+			switch v := v.(*ValidateRouterSdkTerminatorsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_mgmt_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ValidateRouterSdkTerminatorsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_mgmt_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RouterSdkTerminatorsDetails); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_mgmt_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RouterSdkTerminatorDetail); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2469,6 +2857,18 @@ func file_mgmt_proto_init() {
 			}
 		}
 		file_mgmt_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*StreamMetricsRequest_MetricMatcher); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_mgmt_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*StreamMetricsEvent_IntervalMetric); i {
 			case 0:
 				return &v.state
@@ -2480,7 +2880,7 @@ func file_mgmt_proto_init() {
 				return nil
 			}
 		}
-		file_mgmt_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
+		file_mgmt_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*InspectResponse_InspectValue); i {
 			case 0:
 				return &v.state
@@ -2500,7 +2900,7 @@ func file_mgmt_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_mgmt_proto_rawDesc,
 			NumEnums:      6,
-			NumMessages:   25,
+			NumMessages:   29,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
diff --git a/common/pb/mgmt_pb/mgmt.proto b/common/pb/mgmt_pb/mgmt.proto
index 7290097a1..383d186e8 100644
--- a/common/pb/mgmt_pb/mgmt.proto
+++ b/common/pb/mgmt_pb/mgmt.proto
@@ -54,6 +54,10 @@ enum ContentType {
   ValidateRouterLinksResponseType = 10104;
   ValidateRouterLinksResultType = 10105;
 
+  ValidateRouterSdkTerminatorsRequestType = 10106;
+  ValidateRouterSdkTerminatorsResponseType = 10107;
+  ValidateRouterSdkTerminatorsResultType = 10108;
+
 }
 
 enum Header {
@@ -237,3 +241,31 @@ message RouterLinkDetail {
   string destRouterId = 6;
   bool dialed = 7;
 }
+
+message ValidateRouterSdkTerminatorsRequest {
+  string filter = 1;
+}
+
+message ValidateRouterSdkTerminatorsResponse {
+  bool success = 1;
+  string message = 2;
+  uint64 routerCount = 3;
+}
+
+message RouterSdkTerminatorsDetails {
+  string routerId = 1;
+  string routerName = 2;
+  bool validateSuccess = 3;
+  string message = 4;
+  repeated RouterSdkTerminatorDetail details = 5;
+}
+
+message RouterSdkTerminatorDetail {
+  string terminatorId = 1;
+  TerminatorState ctrlState = 2;
+  string routerState = 3;
+  bool isValid = 4;
+  bool operaationActive = 5;
+  string createTime = 6;
+  string lastAttempt = 7;
+}
\ No newline at end of file
diff --git a/controller/api_impl/circuit_api_model.go b/controller/api_impl/circuit_api_model.go
index aa4428f5e..a0400eb0f 100644
--- a/controller/api_impl/circuit_api_model.go
+++ b/controller/api_impl/circuit_api_model.go
@@ -53,17 +53,26 @@ func MapCircuitToRestModel(n *network.Network, _ api.RequestContext, circuit *ne
 		path.Links = append(path.Links, ToEntityRef(link.Id, link, LinkLinkFactory))
 	}
 
-	svc, err := n.Services.Read(circuit.ServiceId)
-	if err != nil {
-		return nil, err
+	var svcEntityRef *rest_model.EntityRef
+	if svc, _ := n.Services.Read(circuit.ServiceId); svc != nil {
+		svcEntityRef = ToEntityRef(svc.Name, svc, ServiceLinkFactory)
+	} else {
+		svcEntityRef = ToEntityRef("<deleted>", deletedEntity(circuit.ServiceId), ServiceLinkFactory)
 	}
+
 	ret := &rest_model.CircuitDetail{
 		BaseEntity: BaseEntityToRestModel(circuit, CircuitLinkFactory),
 		ClientID:   circuit.ClientId,
 		Path:       path,
-		Service:    ToEntityRef(svc.Name, svc, ServiceLinkFactory),
+		Service:    svcEntityRef,
 		Terminator: ToEntityRef(circuit.Terminator.GetId(), circuit.Terminator, TerminatorLinkFactory),
 	}
 
 	return ret, nil
 }
+
+type deletedEntity string
+
+func (self deletedEntity) GetId() string {
+	return string(self)
+}
diff --git a/controller/change/util.go b/controller/change/util.go
new file mode 100644
index 000000000..8b83d8c1a
--- /dev/null
+++ b/controller/change/util.go
@@ -0,0 +1,30 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package change
+
+import "github.com/openziti/channel/v2"
+
+func NewControlChannelChange(routerId, routerName, method string, ch channel.Channel) *Context {
+	return New().
+		SetChangeAuthorId(routerId).
+		SetChangeAuthorName(routerName).
+		SetChangeAuthorType(AuthorTypeRouter).
+		SetSourceType(SourceTypeControlChannel).
+		SetSourceMethod(method).
+		SetSourceLocal(ch.Underlay().GetLocalAddr().String()).
+		SetSourceRemote(ch.Underlay().GetRemoteAddr().String())
+}
diff --git a/controller/command/command.go b/controller/command/command.go
index c6eb26cb0..d90af365f 100644
--- a/controller/command/command.go
+++ b/controller/command/command.go
@@ -51,6 +51,7 @@ type Dispatcher interface {
 	Dispatch(command Command) error
 	IsLeaderOrLeaderless() bool
 	GetPeers() map[string]channel.Channel
+	GetRateLimiter() RateLimiter
 }
 
 // LocalDispatcher should be used when running a non-clustered system
@@ -67,6 +68,10 @@ func (self *LocalDispatcher) GetPeers() map[string]channel.Channel {
 	return nil
 }
 
+func (self *LocalDispatcher) GetRateLimiter() RateLimiter {
+	return self.Limiter
+}
+
 func (self *LocalDispatcher) Dispatch(command Command) error {
 	defer func() {
 		if p := recover(); p != nil {
diff --git a/controller/command/rate_limiter.go b/controller/command/rate_limiter.go
index 1ce31d186..f62202c78 100644
--- a/controller/command/rate_limiter.go
+++ b/controller/command/rate_limiter.go
@@ -18,10 +18,13 @@ package command
 
 import (
 	"fmt"
+	"github.com/google/uuid"
+	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/foundation/v2/errorz"
 	"github.com/openziti/metrics"
 	"github.com/openziti/ziti/controller/apierror"
 	"github.com/pkg/errors"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -33,6 +36,11 @@ const (
 
 	DefaultLimiterSize = 100
 	MinLimiterSize     = 10
+
+	DefaultAdaptiveRateLimiterEnabled       = true
+	DefaultAdaptiveRateLimiterMinWindowSize = 5
+	DefaultAdaptiveRateLimiterMaxWindowSize = 250
+	DefaultAdaptiveRateLimiterTimeout       = 30 * time.Second
 )
 
 type RateLimiterConfig struct {
@@ -53,6 +61,7 @@ func NewRateLimiter(config RateLimiterConfig, registry metrics.Registry, closeNo
 		queue:       make(chan *rateLimitedWork, config.QueueSize),
 		closeNotify: closeNotify,
 		workRate:    registry.Timer(MetricCommandLimiterWorkTimer),
+		config:      config,
 	}
 
 	if existing := registry.GetGauge(MetricCommandLimiterCurrentQueuedCount); existing != nil {
@@ -73,6 +82,7 @@ func NewRateLimiter(config RateLimiterConfig, registry metrics.Registry, closeNo
 // an ApiError indicating that the server is too busy
 type RateLimiter interface {
 	RunRateLimited(func() error) error
+	GetQueueFillPct() float64
 }
 
 // An AdaptiveRateLimiter allows running arbitrary, sequential operations with a limiter, so that only N operations
@@ -96,18 +106,46 @@ type AdaptiveRateLimiter interface {
 	RunRateLimited(f func() error) (RateLimitControl, error)
 }
 
+// An AdaptiveRateLimitTracker works similarly to an AdaptiveRateLimiter, except it just manages the rate
+// limiting without actually running the work. Because it doesn't run the work itself, it has to account
+// for the possibility that some work may never report as complete or failed. It thus has a configurable
+// timeout at which point outstanding work will be marked as failed.
+type AdaptiveRateLimitTracker interface {
+	RunRateLimited(label string) (RateLimitControl, error)
+	RunRateLimitedF(label string, f func(control RateLimitControl) error) error
+	IsRateLimited() bool
+}
+
 type NoOpRateLimiter struct{}
 
 func (self NoOpRateLimiter) RunRateLimited(f func() error) error {
 	return f()
 }
 
+func (self NoOpRateLimiter) GetQueueFillPct() float64 {
+	return 0
+}
+
 type NoOpAdaptiveRateLimiter struct{}
 
 func (self NoOpAdaptiveRateLimiter) RunRateLimited(f func() error) (RateLimitControl, error) {
 	return noOpRateLimitControl{}, f()
 }
 
+type NoOpAdaptiveRateLimitTracker struct{}
+
+func (n NoOpAdaptiveRateLimitTracker) RunRateLimited(string) (RateLimitControl, error) {
+	return noOpRateLimitControl{}, nil
+}
+
+func (n NoOpAdaptiveRateLimitTracker) RunRateLimitedF(_ string, f func(control RateLimitControl) error) error {
+	return f(noOpRateLimitControl{})
+}
+
+func (n NoOpAdaptiveRateLimitTracker) IsRateLimited() bool {
+	return false
+}
+
 type rateLimitedWork struct {
 	wrapped func() error
 	result  chan error
@@ -118,6 +156,7 @@ type DefaultRateLimiter struct {
 	queue       chan *rateLimitedWork
 	closeNotify <-chan struct{}
 	workRate    metrics.Timer
+	config      RateLimiterConfig
 }
 
 func (self *DefaultRateLimiter) RunRateLimited(f func() error) error {
@@ -141,6 +180,10 @@ func (self *DefaultRateLimiter) RunRateLimited(f func() error) error {
 	}
 }
 
+func (self *DefaultRateLimiter) GetQueueFillPct() float64 {
+	return float64(self.currentSize.Load()) / float64(self.config.QueueSize)
+}
+
 func (self *DefaultRateLimiter) run() {
 	defer self.workRate.Dispose()
 
@@ -180,19 +223,58 @@ type AdaptiveRateLimiterConfig struct {
 
 	// WindowSizeMetric - the name of the metric show the current window size
 	WindowSizeMetric string
+
+	// Timeout - only used for AdaptiveRateLimitTracker, sets when a piece of outstanding work will be assumed to
+	//           have failed if it hasn't been marked completed yet, so that work slots aren't lost
+	Timeout time.Duration
+}
+
+func (self *AdaptiveRateLimiterConfig) SetDefaults() {
+	self.Enabled = DefaultAdaptiveRateLimiterEnabled
+	self.MinSize = DefaultAdaptiveRateLimiterMinWindowSize
+	self.MaxSize = DefaultAdaptiveRateLimiterMaxWindowSize
+	self.Timeout = DefaultAdaptiveRateLimiterTimeout
 }
 
-func (self *AdaptiveRateLimiterConfig) Validate() error {
-	if !self.Enabled {
-		return nil
+func LoadAdaptiveRateLimiterConfig(cfg *AdaptiveRateLimiterConfig, cfgmap map[interface{}]interface{}) error {
+	if value, found := cfgmap["enabled"]; found {
+		cfg.Enabled = strings.EqualFold("true", fmt.Sprintf("%v", value))
+	}
+
+	if value, found := cfgmap["maxSize"]; found {
+		if intVal, ok := value.(int); ok {
+			v := int64(intVal)
+			cfg.MaxSize = uint32(v)
+		} else {
+			return errors.Errorf("invalid value %d for adaptive rate limiter max size, must be integer value", value)
+		}
+	}
+
+	if value, found := cfgmap["minSize"]; found {
+		if intVal, ok := value.(int); ok {
+			v := int64(intVal)
+			cfg.MinSize = uint32(v)
+		} else {
+			return errors.Errorf("invalid value %d for adaptive rate limiter min size, must be integer value", value)
+		}
+	}
+
+	if cfg.MinSize < 1 {
+		return errors.Errorf("invalid value %d for adaptive rate limiter min size, must be at least", cfg.MinSize)
 	}
 
-	if self.MinSize < 1 {
-		return errors.New("adaptive rate limiter min size is 1")
+	if cfg.MinSize > cfg.MaxSize {
+		return errors.Errorf("invalid values, %d, %d for adaptive rate limiter min size and max size, min must be <= max",
+			cfg.MinSize, cfg.MaxSize)
 	}
-	if self.MinSize > self.MaxSize {
-		return fmt.Errorf("adaptive rate limiter min size must be <- max size. min: %v, max: %v", self.MinSize, self.MaxSize)
+
+	if value, found := cfgmap["timeout"]; found {
+		var err error
+		if cfg.Timeout, err = time.ParseDuration(fmt.Sprintf("%v", value)); err != nil {
+			return fmt.Errorf("invalid value %v for adaptive rate limiter timeout (%w)", value, err)
+		}
 	}
+
 	return nil
 }
 
@@ -202,12 +284,11 @@ func NewAdaptiveRateLimiter(config AdaptiveRateLimiterConfig, registry metrics.R
 	}
 
 	result := &adaptiveRateLimiter{
-		currentWindow: atomic.Int32{},
-		minWindow:     int32(config.MinSize),
-		maxWindow:     int32(config.MaxSize),
-		queue:         make(chan *adaptiveRateLimitedWork, config.MaxSize),
-		closeNotify:   closeNotify,
-		workRate:      registry.Timer(config.WorkTimerMetric),
+		minWindow:   int32(config.MinSize),
+		maxWindow:   int32(config.MaxSize),
+		queue:       make(chan *adaptiveRateLimitedWork, config.MaxSize),
+		closeNotify: closeNotify,
+		workRate:    registry.Timer(config.WorkTimerMetric),
 	}
 
 	if existing := registry.GetGauge(config.QueueSizeMetric); existing != nil {
@@ -267,7 +348,7 @@ func (self *adaptiveRateLimiter) success() {
 	}
 }
 
-func (self *adaptiveRateLimiter) failure(queuePosition int32) {
+func (self *adaptiveRateLimiter) backoff(queuePosition int32) {
 	if self.currentWindow.Load() <= self.minWindow {
 		return
 	}
@@ -351,8 +432,14 @@ func (self *adaptiveRateLimiter) run() {
 }
 
 type RateLimitControl interface {
+	// Success indicates the operation was a success
 	Success()
-	Timeout()
+
+	// Backoff indicates that we need to backoff
+	Backoff()
+
+	// Failed indicates the operation was not a success, but a backoff isn't required
+	Failed()
 }
 
 type rateLimitControl struct {
@@ -364,15 +451,25 @@ func (r rateLimitControl) Success() {
 	r.limiter.success()
 }
 
-func (r rateLimitControl) Timeout() {
-	r.limiter.failure(r.queuePosition)
+func (r rateLimitControl) Backoff() {
+	r.limiter.backoff(r.queuePosition)
+}
+
+func (r rateLimitControl) Failed() {
+	// no-op for this type
+}
+
+func NoOpRateLimitControl() RateLimitControl {
+	return noOpRateLimitControl{}
 }
 
 type noOpRateLimitControl struct{}
 
 func (noOpRateLimitControl) Success() {}
 
-func (noOpRateLimitControl) Timeout() {}
+func (noOpRateLimitControl) Backoff() {}
+
+func (noOpRateLimitControl) Failed() {}
 
 func WasRateLimited(err error) bool {
 	var apiErr *errorz.ApiError
@@ -381,3 +478,207 @@ func WasRateLimited(err error) bool {
 	}
 	return false
 }
+
+func NewAdaptiveRateLimitTracker(config AdaptiveRateLimiterConfig, registry metrics.Registry, closeNotify <-chan struct{}) AdaptiveRateLimitTracker {
+	if !config.Enabled {
+		return NoOpAdaptiveRateLimitTracker{}
+	}
+
+	result := &adaptiveRateLimitTracker{
+		minWindow:       int32(config.MinSize),
+		maxWindow:       int32(config.MaxSize),
+		timeout:         config.Timeout,
+		workRate:        registry.Timer(config.WorkTimerMetric),
+		outstandingWork: map[string]*adaptiveRateLimitTrackerWork{},
+		closeNotify:     closeNotify,
+	}
+
+	if existing := registry.GetGauge(config.QueueSizeMetric); existing != nil {
+		existing.Dispose()
+	}
+
+	registry.FuncGauge(config.QueueSizeMetric, func() int64 {
+		return int64(result.currentSize.Load())
+	})
+
+	if existing := registry.GetGauge(config.WindowSizeMetric); existing != nil {
+		existing.Dispose()
+	}
+
+	registry.FuncGauge(config.WindowSizeMetric, func() int64 {
+		return int64(result.currentWindow.Load())
+	})
+
+	result.currentWindow.Store(int32(config.MaxSize))
+
+	go result.run()
+
+	return result
+}
+
+type adaptiveRateLimitTracker struct {
+	currentWindow  atomic.Int32
+	minWindow      int32
+	maxWindow      int32
+	timeout        time.Duration
+	lock           sync.Mutex
+	successCounter atomic.Uint32
+
+	currentSize     atomic.Int32
+	workRate        metrics.Timer
+	outstandingWork map[string]*adaptiveRateLimitTrackerWork
+	closeNotify     <-chan struct{}
+}
+
+func (self *adaptiveRateLimitTracker) IsRateLimited() bool {
+	return self.currentSize.Load() >= self.currentWindow.Load()
+}
+
+func (self *adaptiveRateLimitTracker) success(work *adaptiveRateLimitTrackerWork) {
+	self.lock.Lock()
+	defer self.lock.Unlock()
+
+	self.currentSize.Add(-1)
+	delete(self.outstandingWork, work.id)
+	self.workRate.UpdateSince(work.createTime)
+	if self.currentWindow.Load() >= self.maxWindow {
+		return
+	}
+
+	if self.successCounter.Add(1)%10 == 0 {
+		if nextVal := self.currentWindow.Add(1); nextVal > self.maxWindow {
+			self.currentWindow.Store(self.maxWindow)
+		}
+	}
+}
+
+func (self *adaptiveRateLimitTracker) backoff(work *adaptiveRateLimitTrackerWork) {
+	self.lock.Lock()
+	defer self.lock.Unlock()
+
+	self.currentSize.Add(-1)
+	delete(self.outstandingWork, work.id)
+
+	if self.currentWindow.Load() <= self.minWindow {
+		return
+	}
+
+	current := self.currentWindow.Load()
+	nextWindow := work.queuePosition - 10
+	if nextWindow < current {
+		if nextWindow < self.minWindow {
+			nextWindow = self.minWindow
+		}
+		self.currentWindow.Store(nextWindow)
+	}
+}
+
+func (self *adaptiveRateLimitTracker) complete(work *adaptiveRateLimitTrackerWork) {
+	self.lock.Lock()
+	defer self.lock.Unlock()
+	self.currentSize.Add(-1)
+	delete(self.outstandingWork, work.id)
+}
+
+func (self *adaptiveRateLimitTracker) RunRateLimited(label string) (RateLimitControl, error) {
+	self.lock.Lock()
+	defer self.lock.Unlock()
+	queuePosition := self.currentSize.Add(1)
+	if queuePosition > self.currentWindow.Load() {
+		self.currentSize.Add(-1)
+		return noOpRateLimitControl{}, apierror.NewTooManyUpdatesError()
+	}
+
+	work := &adaptiveRateLimitTrackerWork{
+		id:            uuid.NewString(),
+		limiter:       self,
+		queuePosition: queuePosition,
+		createTime:    time.Now(),
+		label:         label,
+	}
+
+	self.outstandingWork[work.id] = work
+
+	return work, nil
+}
+
+func (self *adaptiveRateLimitTracker) RunRateLimitedF(label string, f func(control RateLimitControl) error) error {
+	ctrl, err := self.RunRateLimited(label)
+	if err != nil {
+		return err
+	}
+	return f(ctrl)
+}
+
+func (self *adaptiveRateLimitTracker) run() {
+	defer self.workRate.Dispose()
+
+	ticker := time.NewTicker(30 * time.Second)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			self.cleanExpired()
+		case <-self.closeNotify:
+			return
+		}
+	}
+}
+
+func (self *adaptiveRateLimitTracker) cleanExpired() {
+	self.lock.Lock()
+
+	var toRemove []*adaptiveRateLimitTrackerWork
+
+	for _, v := range self.outstandingWork {
+		if time.Since(v.createTime) > self.timeout {
+			toRemove = append(toRemove, v)
+		}
+	}
+
+	self.lock.Unlock()
+
+	for _, work := range toRemove {
+		pfxlog.Logger().WithField("label", work.label).
+			WithField("duration", time.Since(work.createTime)).
+			Error("rate limit work expired")
+		work.Backoff()
+	}
+}
+
+type adaptiveRateLimitTrackerWork struct {
+	id            string
+	limiter       *adaptiveRateLimitTracker
+	queuePosition int32
+	createTime    time.Time
+	completed     atomic.Bool
+	label         string
+}
+
+func (self *adaptiveRateLimitTrackerWork) Success() {
+	if self.completed.CompareAndSwap(false, true) {
+		pfxlog.Logger().WithField("label", self.label).
+			WithField("duration", time.Since(self.createTime)).
+			Info("success")
+		self.limiter.success(self)
+	}
+}
+
+func (self *adaptiveRateLimitTrackerWork) Backoff() {
+	if self.completed.CompareAndSwap(false, true) {
+		pfxlog.Logger().WithField("label", self.label).
+			WithField("duration", time.Since(self.createTime)).
+			Info("backoff")
+		self.limiter.backoff(self)
+	}
+}
+
+func (self *adaptiveRateLimitTrackerWork) Failed() {
+	if self.completed.CompareAndSwap(false, true) {
+		pfxlog.Logger().WithField("label", self.label).
+			WithField("duration", time.Since(self.createTime)).
+			Info("failed")
+		self.limiter.complete(self)
+	}
+}
diff --git a/controller/command/rate_limiter_test.go b/controller/command/rate_limiter_test.go
index 31937f71a..c6384b1f3 100644
--- a/controller/command/rate_limiter_test.go
+++ b/controller/command/rate_limiter_test.go
@@ -21,6 +21,7 @@ package command
 import (
 	"errors"
 	"fmt"
+	"github.com/openziti/foundation/v2/concurrenz"
 	"github.com/openziti/foundation/v2/errorz"
 	"github.com/openziti/metrics"
 	"github.com/openziti/sdk-golang/ziti"
@@ -88,7 +89,7 @@ func Test_AdaptiveRateLimiter(t *testing.T) {
 					elapsed := time.Since(start)
 					if elapsed > time.Second*5 {
 						timedOut.Add(1)
-						ctrl.Timeout()
+						ctrl.Backoff()
 					} else {
 						count++
 						completed.Add(1)
@@ -111,6 +112,90 @@ func Test_AdaptiveRateLimiter(t *testing.T) {
 	logStats()
 }
 
+func Test_AdaptiveRateLimiterTracker(t *testing.T) {
+	cfg := AdaptiveRateLimiterConfig{
+		Enabled:          true,
+		MaxSize:          250,
+		MinSize:          5,
+		WorkTimerMetric:  "workTime",
+		QueueSizeMetric:  "queueSize",
+		WindowSizeMetric: "windowSize",
+		Timeout:          time.Second,
+	}
+
+	registry := metrics.NewRegistry("test", nil)
+	closeNotify := make(chan struct{})
+	limiter := NewAdaptiveRateLimitTracker(cfg, registry, closeNotify).(*adaptiveRateLimitTracker)
+
+	var queueFull atomic.Uint32
+	var timedOut atomic.Uint32
+	var completed atomic.Uint32
+
+	countdown := &sync.WaitGroup{}
+
+	logStats := func() {
+		fmt.Printf("queueFulls: %v\n", queueFull.Load())
+		fmt.Printf("timedOut: %v\n", timedOut.Load())
+		fmt.Printf("completed: %v\n", completed.Load())
+		fmt.Printf("queueSize: %v\n", limiter.currentSize.Load())
+		fmt.Printf("windowSize: %v\n", limiter.currentWindow.Load())
+	}
+
+	go func() {
+		for {
+			select {
+			case <-closeNotify:
+				return
+			case <-time.After(time.Second):
+				logStats()
+			}
+		}
+	}()
+
+	sem := concurrenz.NewSemaphore(25)
+
+	for i := 0; i < 300; i++ {
+		countdown.Add(1)
+
+		go func() {
+			defer countdown.Done()
+			count := 0
+			for count < 1000 {
+				// start := time.Now()
+				err := limiter.RunRateLimitedF("", func(control RateLimitControl) error {
+					if sem.TryAcquire() {
+						time.Sleep(25 * time.Millisecond)
+						control.Success()
+						sem.Release()
+						completed.Add(1)
+					} else {
+						time.Sleep(5 * time.Millisecond)
+						control.Backoff()
+						timedOut.Add(1)
+					}
+					return nil
+				})
+
+				if err != nil {
+					apiError := &errorz.ApiError{}
+					if errors.As(err, &apiError) && apiError.Code == apierror.ServerTooManyRequestsCode {
+						queueFull.Add(1)
+						time.Sleep(time.Millisecond)
+					} else {
+						panic(err)
+					}
+				} else {
+					count++
+				}
+			}
+		}()
+	}
+
+	countdown.Wait()
+	close(closeNotify)
+	logStats()
+}
+
 func Test_AuthFlood(t *testing.T) {
 	countdown := &sync.WaitGroup{}
 
diff --git a/controller/config/config.go b/controller/config/config.go
index be1e92368..ed62d6c0a 100644
--- a/controller/config/config.go
+++ b/controller/config/config.go
@@ -401,53 +401,40 @@ func (c *Config) loadEnrollmentSection(edgeConfigMap map[interface{}]interface{}
 	return nil
 }
 
-func (c *Config) loadAuthRateLimiter(cfgmap map[interface{}]interface{}) error {
+func (c *Config) loadAuthRateLimiterConfig(cfgmap map[interface{}]interface{}) error {
+	c.AuthRateLimiter.SetDefaults()
+
 	c.AuthRateLimiter.Enabled = DefaultAuthRateLimiterEnabled
 	c.AuthRateLimiter.MaxSize = DefaultAuthRateLimiterMaxSize
 	c.AuthRateLimiter.MinSize = DefaultAuthRateLimiterMinSize
 
 	if value, found := cfgmap["authRateLimiter"]; found {
 		if submap, ok := value.(map[interface{}]interface{}); ok {
-			if value, found := submap["enabled"]; found {
-				c.AuthRateLimiter.Enabled = strings.EqualFold("true", fmt.Sprintf("%v", value))
+			if err := command.LoadAdaptiveRateLimiterConfig(&c.AuthRateLimiter, submap); err != nil {
+				return err
 			}
-
-			if value, found := submap["maxSize"]; found {
-				if intVal, ok := value.(int); ok {
-					v := int64(intVal)
-					if v < AuthRateLimiterMinSizeValue {
-						return errors.Errorf("invalid value %v for authRateLimiter.maxSize, must be at least %v", value, AuthRateLimiterMinSizeValue)
-					}
-					if v > AuthRateLimiterMaxSizeValue {
-						return errors.Errorf("invalid value %v for authRateLimiter.maxSize, must be at most %v", value, AuthRateLimiterMaxSizeValue)
-					}
-					c.AuthRateLimiter.MaxSize = uint32(v)
-				} else {
-					return errors.Errorf("invalid value %v for authRateLimiter.maxSize, must be integer value", value)
-				}
+			if c.AuthRateLimiter.MaxSize < AuthRateLimiterMinSizeValue {
+				return errors.Errorf("invalid value %v for authRateLimiter.maxSize, must be at least %v",
+					c.AuthRateLimiter.MaxSize, AuthRateLimiterMinSizeValue)
 			}
-
-			if value, found := submap["minSize"]; found {
-				if intVal, ok := value.(int); ok {
-					v := int64(intVal)
-					if v < AuthRateLimiterMinSizeValue {
-						return errors.Errorf("invalid value %v for authRateLimiter.minSize, must be at least %v", value, AuthRateLimiterMinSizeValue)
-					}
-					if v > AuthRateLimiterMaxSizeValue {
-						return errors.Errorf("invalid value %v for authRateLimiter.minSize, must be at most %v", value, AuthRateLimiterMaxSizeValue)
-					}
-					c.AuthRateLimiter.MinSize = uint32(v)
-				} else {
-					return errors.Errorf("invalid value %v for authRateLimiter.minSize, must be integer value", value)
-				}
+			if c.AuthRateLimiter.MaxSize > AuthRateLimiterMaxSizeValue {
+				return errors.Errorf("invalid value %v for authRateLimiter.maxSize, must be at most %v",
+					c.AuthRateLimiter.MaxSize, AuthRateLimiterMaxSizeValue)
 			}
 
-			if c.AuthRateLimiter.MinSize > c.AuthRateLimiter.MaxSize {
-				return errors.Errorf("invalid values, %v, %v for authRateLimiter minSize and maxSize, min must be <= max",
-					c.AuthRateLimiter.MinSize, c.AuthRateLimiter.MaxSize)
+			if c.AuthRateLimiter.MinSize < AuthRateLimiterMinSizeValue {
+				return errors.Errorf("invalid value %v for authRateLimiter.minSize, must be at least %v",
+					c.AuthRateLimiter.MinSize, AuthRateLimiterMinSizeValue)
 			}
+			if c.AuthRateLimiter.MinSize > AuthRateLimiterMaxSizeValue {
+				return errors.Errorf("invalid value %v for authRateLimiter.minSize, must be at most %v",
+					c.AuthRateLimiter.MinSize, AuthRateLimiterMaxSizeValue)
+			}
+		} else {
+			return errors.Errorf("invalid type for authRateLimiter, should be map instead of %T", value)
 		}
 	}
+
 	return nil
 }
 
@@ -484,7 +471,7 @@ func LoadFromMap(configMap map[interface{}]interface{}) (*Config, error) {
 		return nil, err
 	}
 
-	if err = edgeConfig.loadAuthRateLimiter(edgeConfigMap); err != nil {
+	if err = edgeConfig.loadAuthRateLimiterConfig(edgeConfigMap); err != nil {
 		return nil, err
 	}
 
diff --git a/controller/db/api_session_store.go b/controller/db/api_session_store.go
index 1aaad7937..52e6c4b95 100644
--- a/controller/db/api_session_store.go
+++ b/controller/db/api_session_store.go
@@ -23,6 +23,8 @@ import (
 	"github.com/openziti/storage/boltz"
 	"github.com/openziti/ziti/common/eid"
 	"github.com/openziti/ziti/controller/change"
+	"github.com/pkg/errors"
+	log "github.com/sirupsen/logrus"
 	"go.etcd.io/bbolt"
 	"strings"
 	"time"
@@ -149,44 +151,43 @@ func (store *apiSessionStoreImpl) onEventualDelete(db boltz.Db, name string, api
 		}).Error("error querying for session associated to an api session during onEventualDelete")
 	}
 
-	for _, id := range idCollector.ids {
-		changeContext := change.New().SetSourceType("events.emitter").SetChangeAuthorType(change.AuthorTypeController)
-		err = db.Update(changeContext.NewMutateContext(), func(ctx boltz.MutateContext) error {
-			if err := store.stores.session.DeleteById(ctx, id); err != nil {
-				if boltz.IsErrNotFoundErr(err) {
-					return nil
-				}
-				return err
-			}
-			return nil
-		})
-
-		if err != nil {
-			pfxlog.Logger().WithError(err).WithFields(map[string]interface{}{
-				"eventName":    name,
-				"apiSessionId": string(apiSessionId),
-				"sessionId":    id,
-			}).Error("error deleting for session associated to an api session during onEventualDelete")
-		}
+	if store.stores.rateLimiter.GetQueueFillPct() > 0.5 {
+		time.Sleep(time.Second)
 	}
 
+	store.cleanupSessions(db, name, apiSessionId, idCollector.ids)
+}
+
+func (store *apiSessionStoreImpl) cleanupSessions(db boltz.Db, name string, apiSessionId []byte, sessionIds []string) {
+	logger := pfxlog.Logger().WithField("eventName", name).
+		WithField("apiSessionId", string(apiSessionId))
+
 	changeContext := change.New().SetSourceType("events.emitter").SetChangeAuthorType(change.AuthorTypeController)
-	err = db.Update(changeContext.NewMutateContext(), func(ctx boltz.MutateContext) error {
+	err := db.Update(changeContext.NewMutateContext(), func(ctx boltz.MutateContext) error {
+		indexPath := []string{RootBucket, boltz.IndexesBucket, EntityTypeApiSessions, EntityTypeSessions}
 		if bucket := boltz.Path(ctx.Tx(), indexPath...); bucket != nil {
 			if err := bucket.DeleteBucket(apiSessionId); err != nil {
-				if err != bbolt.ErrBucketNotFound {
-					return err
+				if !errors.Is(err, bbolt.ErrBucketNotFound) {
+					logger.WithError(err).
+						Error("error deleting for api session index associated to an api session during onEventualDelete")
+				}
+			}
+		}
+
+		for _, id := range sessionIds {
+			if err := store.stores.session.DeleteById(ctx, id); err != nil {
+				if !boltz.IsErrNotFoundErr(err) {
+					logger.WithError(err).WithField("sessionId", id).
+						Error("error deleting for session associated to an api session during onEventualDelete")
 				}
 			}
 		}
+
 		return nil
 	})
 
 	if err != nil {
-		pfxlog.Logger().WithError(err).WithFields(map[string]interface{}{
-			"eventName":    name,
-			"apiSessionId": string(apiSessionId),
-		}).Error("error deleting for api session index associated to an api session during onEventualDelete")
+		log.WithError(err).Error("error while cleanup after api-session delete")
 	}
 }
 
diff --git a/controller/db/stores.go b/controller/db/stores.go
index 89b67ec31..2ee31eebe 100644
--- a/controller/db/stores.go
+++ b/controller/db/stores.go
@@ -24,6 +24,7 @@ import (
 	"github.com/openziti/storage/ast"
 	"github.com/openziti/storage/boltz"
 	"github.com/openziti/ziti/controller/change"
+	"github.com/openziti/ziti/controller/command"
 	"go.etcd.io/bbolt"
 	"go4.org/sort"
 	"reflect"
@@ -219,6 +220,8 @@ type stores struct {
 	postureCheckType        *postureCheckTypeStoreImpl
 	apiSessionCertificate   *ApiSessionCertificateStoreImpl
 	mfa                     *MfaStoreImpl
+
+	rateLimiter command.RateLimiter
 }
 
 type DbProvider interface {
@@ -231,13 +234,15 @@ func (f DbProviderF) GetDb() boltz.Db {
 	return f()
 }
 
-func InitStores(db boltz.Db) (*Stores, error) {
+func InitStores(db boltz.Db, rateLimiter command.RateLimiter) (*Stores, error) {
 	dbProvider := DbProviderF(func() boltz.Db {
 		return db
 	})
 	errorHolder := &errorz.ErrorHolderImpl{}
 
-	internalStores := &stores{}
+	internalStores := &stores{
+		rateLimiter: rateLimiter,
+	}
 
 	internalStores.eventualEvent = newEventualEventStore(internalStores)
 	internalStores.EventualEventer = NewEventualEventerBbolt(dbProvider, internalStores.eventualEvent, 2*time.Second, 1000)
diff --git a/controller/db/testing.go b/controller/db/testing.go
index 47dbfb1f7..88dfbf6e2 100644
--- a/controller/db/testing.go
+++ b/controller/db/testing.go
@@ -7,6 +7,7 @@ import (
 	"github.com/openziti/storage/boltztest"
 	"github.com/openziti/ziti/common/eid"
 	"github.com/openziti/ziti/controller/change"
+	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/controller/xt"
 	"github.com/openziti/ziti/controller/xt_smartrouting"
 	"github.com/pkg/errors"
@@ -40,7 +41,7 @@ func (ctx *TestContext) Init() {
 	ctx.InitDb(Open)
 
 	var err error
-	ctx.stores, err = InitStores(ctx.GetDb())
+	ctx.stores, err = InitStores(ctx.GetDb(), command.NoOpRateLimiter{})
 	ctx.NoError(err)
 
 	ctx.NoError(RunMigrations(ctx.GetDb(), ctx.stores))
diff --git a/controller/events/metrics_mappers.go b/controller/events/metrics_mappers.go
index acb5783a6..94fdf0d2c 100644
--- a/controller/events/metrics_mappers.go
+++ b/controller/events/metrics_mappers.go
@@ -27,9 +27,10 @@ type ctrlChannelMetricsMapper struct{}
 
 func (ctrlChannelMetricsMapper) mapMetrics(_ *metrics_pb.MetricsMessage, event *event.MetricsEvent) {
 	if strings.HasPrefix(event.Metric, "ctrl.") {
-		parts := strings.Split(event.Metric, ":")
-		event.Metric = parts[0]
-		event.SourceEntityId = parts[1]
+		if parts := strings.Split(event.Metric, ":"); len(parts) > 1 {
+			event.Metric = parts[0]
+			event.SourceEntityId = parts[1]
+		}
 	}
 }
 
diff --git a/controller/handler_ctrl/base.go b/controller/handler_ctrl/base.go
index 1ff98b8bf..9614431c8 100644
--- a/controller/handler_ctrl/base.go
+++ b/controller/handler_ctrl/base.go
@@ -28,12 +28,5 @@ type baseHandler struct {
 }
 
 func (self *baseHandler) newChangeContext(ch channel.Channel, method string) *change.Context {
-	return change.New().
-		SetChangeAuthorId(self.router.Id).
-		SetChangeAuthorName(self.router.Name).
-		SetChangeAuthorType(change.AuthorTypeRouter).
-		SetSourceType(change.SourceTypeControlChannel).
-		SetSourceMethod(method).
-		SetSourceLocal(ch.Underlay().GetLocalAddr().String()).
-		SetSourceRemote(ch.Underlay().GetRemoteAddr().String())
+	return change.NewControlChannelChange(self.router.Id, self.router.Name, method, ch)
 }
diff --git a/controller/handler_ctrl/bind.go b/controller/handler_ctrl/bind.go
index af4ec0b95..a71508d22 100644
--- a/controller/handler_ctrl/bind.go
+++ b/controller/handler_ctrl/bind.go
@@ -17,6 +17,7 @@
 package handler_ctrl
 
 import (
+	"github.com/openziti/ziti/common/pb/ctrl_pb"
 	"github.com/sirupsen/logrus"
 	"time"
 
@@ -72,6 +73,10 @@ func (self *bindHandler) BindChannel(binding channel.Binding) error {
 	binding.AddTypedReceiveHandler(newDequiesceRouterHandler(self.router, self.network))
 	binding.AddTypedReceiveHandler(newDecommissionRouterHandler(self.router, self.network))
 	binding.AddTypedReceiveHandler(newPingHandler())
+	binding.AddTypedReceiveHandler(&channel.AsyncFunctionReceiveAdapter{
+		Type:    int32(ctrl_pb.ContentType_ValidateTerminatorsV2ResponseType),
+		Handler: self.network.RouterMessaging.NewValidationResponseHandler(self.network, self.router),
+	})
 	binding.AddPeekHandler(trace.NewChannelPeekHandler(self.network.GetAppId(), binding.GetChannel(), self.network.GetTraceController()))
 	binding.AddPeekHandler(metrics2.NewCtrlChannelPeekHandler(self.router.Id, self.network.GetMetricsRegistry()))
 
diff --git a/controller/handler_ctrl/remove_terminators.go b/controller/handler_ctrl/remove_terminators.go
index 939025eb9..18689b3c4 100644
--- a/controller/handler_ctrl/remove_terminators.go
+++ b/controller/handler_ctrl/remove_terminators.go
@@ -19,9 +19,10 @@ package handler_ctrl
 import (
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/channel/v2"
-	"github.com/openziti/ziti/controller/network"
 	"github.com/openziti/ziti/common/handler_common"
 	"github.com/openziti/ziti/common/pb/ctrl_pb"
+	"github.com/openziti/ziti/controller/command"
+	"github.com/openziti/ziti/controller/network"
 	"google.golang.org/protobuf/proto"
 )
 
@@ -63,6 +64,8 @@ func (self *removeTerminatorsHandler) handleRemoveTerminators(msg *channel.Messa
 			WithField("terminatorIds", request.TerminatorIds).
 			Info("removed terminators")
 		handler_common.SendSuccess(msg, ch, "")
+	} else if command.WasRateLimited(err) {
+		handler_common.SendServerBusy(msg, ch, "remove.terminators")
 	} else {
 		handler_common.SendFailure(msg, ch, err.Error())
 	}
diff --git a/controller/handler_edge_ctrl/create_terminator_v2.go b/controller/handler_edge_ctrl/create_terminator_v2.go
index 26e8a9d82..cd94c09d5 100644
--- a/controller/handler_edge_ctrl/create_terminator_v2.go
+++ b/controller/handler_edge_ctrl/create_terminator_v2.go
@@ -34,6 +34,7 @@ import (
 	"github.com/sirupsen/logrus"
 	"google.golang.org/protobuf/proto"
 	"math"
+	"time"
 )
 
 type createTerminatorV2Handler struct {
@@ -73,6 +74,7 @@ func (self *createTerminatorV2Handler) HandleReceive(msg *channel.Message, ch ch
 }
 
 func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV2RequestContext) {
+	start := time.Now()
 	logger := pfxlog.ContextLogger(self.ch.Label()).
 		WithField("routerId", self.ch.Id()).
 		WithField("token", ctx.req.SessionToken).
@@ -89,7 +91,11 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV
 	ctx.loadService()
 
 	if ctx.err != nil {
-		self.returnError(ctx, edge_ctrl_pb.CreateTerminatorResult_FailedOther, ctx.err, logger)
+		errCode := edge_ctrl_pb.CreateTerminatorResult_FailedOther
+		if errors.Is(ctx.err, InvalidSessionError{}) {
+			errCode = edge_ctrl_pb.CreateTerminatorResult_FailedInvalidSession
+		}
+		self.returnError(ctx, errCode, ctx.err, logger)
 		return
 	}
 
@@ -146,6 +152,7 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV
 			Context: ctx.newChangeContext(),
 		}
 
+		createStart := time.Now()
 		if err := self.appEnv.GetHostController().GetNetwork().Managers.Command.Dispatch(cmd); err != nil {
 			// terminator might have been created while we were trying to create.
 			if terminator, _ = self.getNetwork().Terminators.Read(ctx.req.Address); terminator != nil {
@@ -162,7 +169,9 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV
 				return
 			}
 		} else {
-			logger.WithField("terminator", terminator.Id).Info("created terminator")
+			logger.WithField("terminator", terminator.Id).
+				WithField("createTime", time.Since(createStart)).
+				Info("created terminator")
 		}
 	}
 
@@ -183,7 +192,7 @@ func (self *createTerminatorV2Handler) CreateTerminatorV2(ctx *CreateTerminatorV
 		logger.WithError(err).Error("failed to send CreateTunnelTerminatorResponse")
 	}
 
-	logger.Info("completed create terminator v2 operation")
+	logger.WithField("elapsed", time.Since(start)).Info("completed create terminator v2 operation")
 }
 
 func (self *createTerminatorV2Handler) returnError(ctx *CreateTerminatorV2RequestContext, resultType edge_ctrl_pb.CreateTerminatorResult, err error, logger *logrus.Entry) {
diff --git a/controller/handler_edge_ctrl/errors.go b/controller/handler_edge_ctrl/errors.go
index b4fab5b02..818644cc7 100644
--- a/controller/handler_edge_ctrl/errors.go
+++ b/controller/handler_edge_ctrl/errors.go
@@ -1,3 +1,19 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
 package handler_edge_ctrl
 
 import "github.com/openziti/sdk-golang/ziti/edge"
diff --git a/controller/handler_edge_ctrl/errors_test.go b/controller/handler_edge_ctrl/errors_test.go
new file mode 100644
index 000000000..3857f7413
--- /dev/null
+++ b/controller/handler_edge_ctrl/errors_test.go
@@ -0,0 +1,29 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package handler_edge_ctrl
+
+import (
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func Test_ErrorsIs(t *testing.T) {
+	err := error(InvalidSessionError{})
+	req := require.New(t)
+	req.True(errors.Is(err, InvalidSessionError{}))
+}
diff --git a/controller/handler_mgmt/bind.go b/controller/handler_mgmt/bind.go
index a8a1b9539..2c9837ee4 100644
--- a/controller/handler_mgmt/bind.go
+++ b/controller/handler_mgmt/bind.go
@@ -51,6 +51,12 @@ func (bindHandler *BindHandler) BindChannel(binding channel.Binding) error {
 		Handler: validateLinksRequestHandler.HandleReceive,
 	})
 
+	validateSdkTerminatorsRequestHandler := newValidateRouterSdkTerminatorsHandler(bindHandler.network)
+	binding.AddTypedReceiveHandler(&channel.AsyncFunctionReceiveAdapter{
+		Type:    validateSdkTerminatorsRequestHandler.ContentType(),
+		Handler: validateSdkTerminatorsRequestHandler.HandleReceive,
+	})
+
 	tracesHandler := newStreamTracesHandler(bindHandler.network)
 	binding.AddTypedReceiveHandler(tracesHandler)
 	binding.AddCloseHandler(tracesHandler)
diff --git a/controller/handler_mgmt/validate_router_sdk_terminators.go b/controller/handler_mgmt/validate_router_sdk_terminators.go
new file mode 100644
index 000000000..4f8a02a9e
--- /dev/null
+++ b/controller/handler_mgmt/validate_router_sdk_terminators.go
@@ -0,0 +1,88 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package handler_mgmt
+
+import (
+	"fmt"
+	"github.com/michaelquigley/pfxlog"
+	"github.com/openziti/channel/v2"
+	"github.com/openziti/channel/v2/protobufs"
+	"github.com/openziti/ziti/common/pb/mgmt_pb"
+	"github.com/openziti/ziti/controller/network"
+	"google.golang.org/protobuf/proto"
+	"time"
+)
+
+type validateRouterSdkTerminatorsHandler struct {
+	network *network.Network
+}
+
+func newValidateRouterSdkTerminatorsHandler(network *network.Network) *validateRouterSdkTerminatorsHandler {
+	return &validateRouterSdkTerminatorsHandler{network: network}
+}
+
+func (*validateRouterSdkTerminatorsHandler) ContentType() int32 {
+	return int32(mgmt_pb.ContentType_ValidateRouterSdkTerminatorsRequestType)
+}
+
+func (handler *validateRouterSdkTerminatorsHandler) HandleReceive(msg *channel.Message, ch channel.Channel) {
+	log := pfxlog.ContextLogger(ch.Label())
+	request := &mgmt_pb.ValidateRouterSdkTerminatorsRequest{}
+
+	var err error
+
+	var count int64
+	var evalF func()
+	if err = proto.Unmarshal(msg.Body, request); err == nil {
+		count, evalF, err = handler.network.ValidateRouterSdkTerminators(request.Filter, func(detail *mgmt_pb.RouterSdkTerminatorsDetails) {
+			if !ch.IsClosed() {
+				if sendErr := protobufs.MarshalTyped(detail).WithTimeout(15 * time.Second).SendAndWaitForWire(ch); sendErr != nil {
+					log.WithError(sendErr).Error("send of sdk terminators detail failed, closing channel")
+					if closeErr := ch.Close(); closeErr != nil {
+						log.WithError(closeErr).Error("failed to close channel")
+					}
+				}
+			} else {
+				log.Info("channel closed, unable to send sdk terminators detail")
+			}
+		})
+	}
+
+	response := &mgmt_pb.ValidateRouterSdkTerminatorsResponse{
+		Success:     err == nil,
+		RouterCount: uint64(count),
+	}
+	if err != nil {
+		response.Message = fmt.Sprintf("%v: failed to unmarshall request: %v", handler.network.GetAppId(), err)
+	}
+
+	body, err := proto.Marshal(response)
+	if err != nil {
+		pfxlog.Logger().WithError(err).Error("unexpected error serializing ValidateRouterSdkTerminatorsResponse")
+		return
+	}
+
+	responseMsg := channel.NewMessage(int32(mgmt_pb.ContentType_ValidateRouterSdkTerminatorsResponseType), body)
+	responseMsg.ReplyTo(msg)
+	if err = ch.Send(responseMsg); err != nil {
+		pfxlog.Logger().WithError(err).Error("unexpected error sending ValidateRouterSdkTerminatorsResponse")
+	}
+
+	if evalF != nil {
+		evalF()
+	}
+}
diff --git a/controller/internal/routes/authenticate_router.go b/controller/internal/routes/authenticate_router.go
index 3dcdee71a..3ce97c675 100644
--- a/controller/internal/routes/authenticate_router.go
+++ b/controller/internal/routes/authenticate_router.go
@@ -28,6 +28,7 @@ import (
 	"github.com/openziti/foundation/v2/errorz"
 	"github.com/openziti/metrics"
 	"github.com/openziti/ziti/controller/apierror"
+	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/controller/env"
 	"github.com/openziti/ziti/controller/internal/permissions"
 	"github.com/openziti/ziti/controller/model"
@@ -43,7 +44,8 @@ func init() {
 }
 
 type AuthRouter struct {
-	createTimer metrics.Timer
+	createTimer   metrics.Timer
+	lastAdminAuth concurrenz.AtomicValue[time.Time]
 }
 
 func NewAuthRouter() *AuthRouter {
@@ -181,11 +183,23 @@ func (ro *AuthRouter) authHandler(ae *env.AppEnv, rc *response.RequestContext, h
 
 	var sessionIdHolder concurrenz.AtomicValue[string]
 
-	ctrl, err := ae.AuthRateLimiter.RunRateLimited(func() error {
-		sessionId, err := ae.Managers.ApiSession.Create(changeCtx.NewMutateContext(), newApiSession, sessionCerts)
+	lastAdminAuth := ro.lastAdminAuth.Load()
+	allowAdminBypass := identity.IsAdmin && time.Since(lastAdminAuth) > 10*time.Second &&
+		ro.lastAdminAuth.CompareAndSwap(lastAdminAuth, time.Now())
+
+	var ctrl command.RateLimitControl
+	if allowAdminBypass {
+		var sessionId string
+		sessionId, err = ae.Managers.ApiSession.Create(changeCtx.NewMutateContext(), newApiSession, sessionCerts)
 		sessionIdHolder.Store(sessionId)
-		return err
-	})
+		ctrl = command.NoOpRateLimitControl()
+	} else {
+		ctrl, err = ae.AuthRateLimiter.RunRateLimited(func() error {
+			sessionId, err := ae.Managers.ApiSession.Create(changeCtx.NewMutateContext(), newApiSession, sessionCerts)
+			sessionIdHolder.Store(sessionId)
+			return err
+		})
+	}
 
 	if err != nil {
 		rc.RespondWithError(err)
@@ -223,7 +237,7 @@ func (ro *AuthRouter) authHandler(ae *env.AppEnv, rc *response.RequestContext, h
 	if writeOk {
 		ctrl.Success()
 	} else {
-		ctrl.Timeout()
+		ctrl.Backoff()
 	}
 }
 
diff --git a/controller/internal/routes/identity_router.go b/controller/internal/routes/identity_router.go
index 033c55c28..7fdc82f5d 100644
--- a/controller/internal/routes/identity_router.go
+++ b/controller/internal/routes/identity_router.go
@@ -37,6 +37,7 @@ import (
 	"github.com/openziti/ziti/controller/models"
 	"github.com/openziti/ziti/controller/response"
 	"github.com/sirupsen/logrus"
+	"strings"
 	"time"
 )
 
@@ -109,7 +110,9 @@ func (r *IdentityRouter) Register(ae *env.AppEnv) {
 
 	// service list
 	ae.ManagementApi.IdentityListIdentityServicesHandler = identity.ListIdentityServicesHandlerFunc(func(params identity.ListIdentityServicesParams, _ interface{}) middleware.Responder {
-		return ae.IsAllowed(r.listServices, params.HTTPRequest, params.ID, "", permissions.IsAdmin())
+		return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) {
+			r.listServices(ae, rc, params)
+		}, params.HTTPRequest, params.ID, "", permissions.IsAdmin())
 	})
 
 	// service configs crud
@@ -239,8 +242,19 @@ func (r *IdentityRouter) listServicePolicies(ae *env.AppEnv, rc *response.Reques
 	ListAssociationWithHandler[*model.Identity, *model.ServicePolicy](ae, rc, ae.Managers.Identity, ae.Managers.ServicePolicy, MapServicePolicyToRestEntity)
 }
 
-func (r *IdentityRouter) listServices(ae *env.AppEnv, rc *response.RequestContext) {
-	filterTemplate := `not isEmpty(from servicePolicies where anyOf(identities) = "%v")`
+func (r *IdentityRouter) listServices(ae *env.AppEnv, rc *response.RequestContext, params identity.ListIdentityServicesParams) {
+	typeFilter := ""
+	if params.PolicyType != nil {
+		if strings.EqualFold(*params.PolicyType, db.PolicyTypeBind.String()) {
+			typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeBind.Id())
+		}
+
+		if strings.EqualFold(*params.PolicyType, db.PolicyTypeDial.String()) {
+			typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeDial.Id())
+		}
+	}
+
+	filterTemplate := `not isEmpty(from servicePolicies where anyOf(identities) = "%v"` + typeFilter + ")"
 	ListAssociationsWithFilter[*model.ServiceDetail](ae, rc, filterTemplate, ae.Managers.EdgeService.GetDetailLister(), MapServiceToRestEntity)
 }
 
diff --git a/controller/internal/routes/service_router.go b/controller/internal/routes/service_router.go
index 2f63bfb7c..6ba1d2c38 100644
--- a/controller/internal/routes/service_router.go
+++ b/controller/internal/routes/service_router.go
@@ -17,12 +17,14 @@
 package routes
 
 import (
+	"fmt"
 	"github.com/go-openapi/runtime/middleware"
 	"github.com/michaelquigley/pfxlog"
 	clientService "github.com/openziti/edge-api/rest_client_api_server/operations/service"
 	managementService "github.com/openziti/edge-api/rest_management_api_server/operations/service"
 	"github.com/openziti/metrics"
 	"github.com/openziti/storage/boltz"
+	"github.com/openziti/ziti/controller/db"
 	"github.com/openziti/ziti/controller/fields"
 	"github.com/openziti/ziti/controller/model"
 	"github.com/openziti/ziti/controller/models"
@@ -105,7 +107,9 @@ func (r *ServiceRouter) Register(ae *env.AppEnv) {
 	})
 
 	ae.ManagementApi.ServiceListServiceIdentitiesHandler = managementService.ListServiceIdentitiesHandlerFunc(func(params managementService.ListServiceIdentitiesParams, _ interface{}) middleware.Responder {
-		return ae.IsAllowed(r.listIdentities, params.HTTPRequest, params.ID, "", permissions.IsAdmin())
+		return ae.IsAllowed(func(ae *env.AppEnv, rc *response.RequestContext) {
+			r.listIdentities(ae, rc, params)
+		}, params.HTTPRequest, params.ID, "", permissions.IsAdmin())
 	})
 
 	ae.ManagementApi.ServiceListServiceConfigHandler = managementService.ListServiceConfigHandlerFunc(func(params managementService.ListServiceConfigParams, _ interface{}) middleware.Responder {
@@ -292,8 +296,19 @@ func (r *ServiceRouter) listClientTerminators(ae *env.AppEnv, rc *response.Reque
 	ListTerminatorAssociations(ae, rc, ae.Managers.EdgeService, ae.Managers.Terminator, MapClientTerminatorToRestEntity)
 }
 
-func (r *ServiceRouter) listIdentities(ae *env.AppEnv, rc *response.RequestContext) {
-	filterTemplate := `not isEmpty(from servicePolicies where anyOf(services) = "%v")`
+func (r *ServiceRouter) listIdentities(ae *env.AppEnv, rc *response.RequestContext, params managementService.ListServiceIdentitiesParams) {
+	typeFilter := ""
+	if params.PolicyType != nil {
+		if strings.EqualFold(*params.PolicyType, db.PolicyTypeBind.String()) {
+			typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeBind.Id())
+		}
+
+		if strings.EqualFold(*params.PolicyType, db.PolicyTypeDial.String()) {
+			typeFilter = fmt.Sprintf(` and type = %d`, db.PolicyTypeDial.Id())
+		}
+	}
+
+	filterTemplate := `not isEmpty(from servicePolicies where anyOf(services) = "%v"` + typeFilter + ")"
 	ListAssociationsWithFilter[*model.Identity](ae, rc, filterTemplate, ae.Managers.Identity, MapIdentityToRestEntity)
 }
 
diff --git a/controller/model/identity_manager.go b/controller/model/identity_manager.go
index 40a5dc4ad..2064df14c 100644
--- a/controller/model/identity_manager.go
+++ b/controller/model/identity_manager.go
@@ -801,13 +801,13 @@ func (statusMap *identityStatusMap) HasEdgeRouterConnection(identityId string) b
 			WithField("identityId", identityId).
 			WithField("expiresAt", stat.expiresAt).
 			WithField("now", now).
-			Debugf("reporting identity from active ER conn pool: timedout")
+			Tracef("reporting identity from active ER conn pool: timedout")
 		return ret
 	}
 
 	pfxlog.Logger().
 		WithField("identityId", identityId).
-		Debugf("reporting identity from active ER conn pool: not found")
+		Tracef("reporting identity from active ER conn pool: not found")
 	return false
 }
 
diff --git a/controller/network/network.go b/controller/network/network.go
index 44df5c90f..6808bc56d 100644
--- a/controller/network/network.go
+++ b/controller/network/network.go
@@ -27,6 +27,7 @@ import (
 	fabricMetrics "github.com/openziti/ziti/common/metrics"
 	"github.com/openziti/ziti/common/pb/mgmt_pb"
 	"github.com/openziti/ziti/controller/event"
+	"github.com/openziti/ziti/controller/raft"
 	"os"
 	"path/filepath"
 	"runtime/debug"
@@ -110,7 +111,7 @@ type Network struct {
 }
 
 func NewNetwork(config Config) (*Network, error) {
-	stores, err := db.InitStores(config.GetDb())
+	stores, err := db.InitStores(config.GetDb(), config.GetCommandDispatcher().GetRateLimiter())
 	if err != nil {
 		return nil, err
 	}
@@ -345,28 +346,40 @@ func (network *Network) ValidateTerminators(r *Router) {
 		return
 	}
 
-	var terminators []*ctrl_pb.Terminator
+	network.Managers.RouterMessaging.ValidateRouterTerminators(result.Entities)
+}
 
-	for _, terminator := range result.Entities {
-		terminators = append(terminators, &ctrl_pb.Terminator{
-			Id:      terminator.Id,
-			Binding: terminator.Binding,
-			Address: terminator.Address,
-		})
-	}
+type LinkValidationCallback func(detail *mgmt_pb.RouterLinkDetails)
 
-	req := &ctrl_pb.ValidateTerminatorsRequest{
-		Terminators: terminators,
+func (n *Network) ValidateLinks(filter string, cb LinkValidationCallback) (int64, func(), error) {
+	result, err := n.Routers.BaseList(filter)
+	if err != nil {
+		return 0, nil, err
 	}
 
-	if err = protobufs.MarshalTyped(req).Send(r.Control); err != nil {
-		logger.WithError(err).Error("unexpected error sending ValidateTerminatorsRequest")
+	sem := concurrenz.NewSemaphore(10)
+
+	evalF := func() {
+		for _, router := range result.Entities {
+			connectedRouter := n.GetConnectedRouter(router.Id)
+			if connectedRouter != nil {
+				sem.Acquire()
+				go func() {
+					defer sem.Release()
+					n.linkController.ValidateRouterLinks(n, connectedRouter, cb)
+				}()
+			} else {
+				n.linkController.reportRouterLinksError(router, errors.New("router not connected"), cb)
+			}
+		}
 	}
+
+	return int64(len(result.Entities)), evalF, nil
 }
 
-type LinkValidationCallback func(detail *mgmt_pb.RouterLinkDetails)
+type SdkTerminatorValidationCallback func(detail *mgmt_pb.RouterSdkTerminatorsDetails)
 
-func (n *Network) ValidateLinks(filter string, cb LinkValidationCallback) (int64, func(), error) {
+func (n *Network) ValidateRouterSdkTerminators(filter string, cb SdkTerminatorValidationCallback) (int64, func(), error) {
 	result, err := n.Routers.BaseList(filter)
 	if err != nil {
 		return 0, nil, err
@@ -381,10 +394,10 @@ func (n *Network) ValidateLinks(filter string, cb LinkValidationCallback) (int64
 				sem.Acquire()
 				go func() {
 					defer sem.Release()
-					n.linkController.ValidateRouterLinks(n, connectedRouter, cb)
+					n.Routers.ValidateRouterSdkTerminators(connectedRouter, cb)
 				}()
 			} else {
-				n.linkController.reportRouterLinksError(router, errors.New("router not connected"), cb)
+				n.Routers.reportRouterSdkTerminatorsError(router, errors.New("router not connected"), cb)
 			}
 		}
 	}
@@ -1263,6 +1276,30 @@ func (network *Network) Inspect(name string) (*string, error) {
 		val, err := json.Marshal(result)
 		strVal := string(val)
 		return &strVal, err
+	} else if lc == "connected-peers" {
+		if raftController, ok := network.Dispatcher.(*raft.Controller); ok {
+			members, err := raftController.ListMembers()
+			if err != nil {
+				return nil, err
+			}
+			result, err := json.Marshal(members)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshall cluster member list to json (%w)", err)
+			}
+			resultStr := string(result)
+			return &resultStr, nil
+		}
+	} else if lc == "router-messaging" {
+		routerMessagingState, err := network.Managers.RouterMessaging.Inspect()
+		if err != nil {
+			return nil, err
+		}
+		result, err := json.Marshal(routerMessagingState)
+		if err != nil {
+			return nil, fmt.Errorf("failed to marshall router messaging state to json (%w)", err)
+		}
+		resultStr := string(result)
+		return &resultStr, nil
 	}
 
 	return nil, nil
diff --git a/controller/network/router.go b/controller/network/router.go
index 246d9a8af..b6e9cd62e 100644
--- a/controller/network/router.go
+++ b/controller/network/router.go
@@ -17,16 +17,22 @@
 package network
 
 import (
+	"encoding/json"
+	"fmt"
+	"github.com/openziti/channel/v2/protobufs"
 	"github.com/openziti/foundation/v2/genext"
 	"github.com/openziti/foundation/v2/versions"
+	"github.com/openziti/ziti/common/inspect"
 	"github.com/openziti/ziti/common/pb/cmd_pb"
 	"github.com/openziti/ziti/common/pb/ctrl_pb"
+	"github.com/openziti/ziti/common/pb/mgmt_pb"
 	"github.com/openziti/ziti/controller/change"
 	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/controller/fields"
 	"github.com/openziti/ziti/controller/xt"
 	"google.golang.org/protobuf/proto"
 	"reflect"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -460,6 +466,100 @@ func (self *RouterManager) Unmarshall(bytes []byte) (*Router, error) {
 	}, nil
 }
 
+func (self *RouterManager) ValidateRouterSdkTerminators(router *Router, cb SdkTerminatorValidationCallback) {
+	request := &ctrl_pb.InspectRequest{RequestedValues: []string{"sdk-terminators"}}
+	resp := &ctrl_pb.InspectResponse{}
+	respMsg, err := protobufs.MarshalTyped(request).WithTimeout(time.Minute).SendForReply(router.Control)
+	if err = protobufs.TypedResponse(resp).Unmarshall(respMsg, err); err != nil {
+		self.reportRouterSdkTerminatorsError(router, err, cb)
+		return
+	}
+
+	var inspectResult *inspect.SdkTerminatorInspectResult
+	for _, val := range resp.Values {
+		if val.Name == "sdk-terminators" {
+			if err = json.Unmarshal([]byte(val.Value), &inspectResult); err != nil {
+				self.reportRouterSdkTerminatorsError(router, err, cb)
+				return
+			}
+		}
+	}
+
+	if inspectResult == nil {
+		if len(resp.Errors) > 0 {
+			err = errors.New(strings.Join(resp.Errors, ","))
+			self.reportRouterSdkTerminatorsError(router, err, cb)
+			return
+		}
+		self.reportRouterSdkTerminatorsError(router, errors.New("no terminator details returned from router"), cb)
+		return
+	}
+
+	listResult, err := self.Terminators.BaseList(fmt.Sprintf(`router="%s" and binding="edge" limit none`, router.Id))
+	if err != nil {
+		self.reportRouterSdkTerminatorsError(router, err, cb)
+		return
+	}
+
+	result := &mgmt_pb.RouterSdkTerminatorsDetails{
+		RouterId:        router.Id,
+		RouterName:      router.Name,
+		ValidateSuccess: true,
+	}
+
+	terminators := map[string]*Terminator{}
+
+	for _, terminator := range listResult.Entities {
+		terminators[terminator.Id] = terminator
+	}
+
+	for _, entry := range inspectResult.Entries {
+		detail := &mgmt_pb.RouterSdkTerminatorDetail{
+			TerminatorId:     entry.Id,
+			RouterState:      entry.State,
+			IsValid:          true,
+			OperaationActive: entry.OperationActive,
+			CreateTime:       entry.CreateTime,
+			LastAttempt:      entry.LastAttempt,
+		}
+		result.Details = append(result.Details, detail)
+
+		if entry.State != "established" {
+			detail.IsValid = false
+		}
+
+		if _, found := terminators[entry.Id]; found {
+			detail.CtrlState = mgmt_pb.TerminatorState_Valid
+			delete(terminators, entry.Id)
+		} else {
+			detail.CtrlState = mgmt_pb.TerminatorState_Unknown
+			detail.IsValid = false
+		}
+	}
+
+	for _, terminator := range terminators {
+		detail := &mgmt_pb.RouterSdkTerminatorDetail{
+			TerminatorId: terminator.Id,
+			CtrlState:    mgmt_pb.TerminatorState_Valid,
+			RouterState:  "unknown",
+			IsValid:      false,
+		}
+		result.Details = append(result.Details, detail)
+	}
+
+	cb(result)
+}
+
+func (self *RouterManager) reportRouterSdkTerminatorsError(router *Router, err error, cb SdkTerminatorValidationCallback) {
+	result := &mgmt_pb.RouterSdkTerminatorsDetails{
+		RouterId:        router.Id,
+		RouterName:      router.Name,
+		ValidateSuccess: false,
+		Message:         err.Error(),
+	}
+	cb(result)
+}
+
 type RouterLinks struct {
 	sync.Mutex
 	allLinks     atomic.Value
diff --git a/controller/network/router_messaging.go b/controller/network/router_messaging.go
index 31211390e..3cfd75966 100644
--- a/controller/network/router_messaging.go
+++ b/controller/network/router_messaging.go
@@ -17,11 +17,17 @@
 package network
 
 import (
+	"errors"
 	"github.com/michaelquigley/pfxlog"
+	"github.com/openziti/channel/v2"
 	"github.com/openziti/channel/v2/protobufs"
 	"github.com/openziti/foundation/v2/goroutines"
+	"github.com/openziti/storage/boltz"
+	"github.com/openziti/ziti/common/inspect"
 	"github.com/openziti/ziti/common/pb/ctrl_pb"
-	log "github.com/sirupsen/logrus"
+	"github.com/openziti/ziti/controller/change"
+	"github.com/openziti/ziti/controller/db"
+	"github.com/openziti/ziti/controller/xt"
 	"sync/atomic"
 	"time"
 )
@@ -37,24 +43,49 @@ func (self *routerUpdates) stateUpdated(routerId string) {
 	self.changedRouters[routerId] = struct{}{}
 }
 
+type terminatorInfo struct {
+	xt.Terminator
+	marker uint64
+}
+type terminatorValidations struct {
+	terminators     map[string]terminatorInfo
+	checkInProgress atomic.Bool
+	lastSend        time.Time
+}
+
 type routerEvent interface {
 	handle(c *RouterMessaging)
 }
 
 func NewRouterMessaging(managers *Managers, routerCommPool goroutines.Pool) *RouterMessaging {
-	return &RouterMessaging{
-		managers:       managers,
-		eventsC:        make(chan routerEvent, 16),
-		routers:        map[string]*routerUpdates{},
-		routerCommPool: routerCommPool,
+	result := &RouterMessaging{
+		managers:              managers,
+		eventsC:               make(chan routerEvent, 16),
+		routerUpdates:         map[string]*routerUpdates{},
+		terminatorValidations: map[string]*terminatorValidations{},
+		routerCommPool:        routerCommPool,
 	}
+
+	managers.stores.Terminator.AddEntityEventListenerF(result.TerminatorCreated, boltz.EntityCreated)
+
+	return result
 }
 
 type RouterMessaging struct {
-	managers       *Managers
-	eventsC        chan routerEvent
-	routers        map[string]*routerUpdates
-	routerCommPool goroutines.Pool
+	managers              *Managers
+	eventsC               chan routerEvent
+	routerUpdates         map[string]*routerUpdates
+	terminatorValidations map[string]*terminatorValidations
+	routerCommPool        goroutines.Pool
+	markerCounter         atomic.Uint64
+}
+
+func (self *RouterMessaging) getNextMarker() uint64 {
+	result := self.markerCounter.Add(1)
+	for result == 0 {
+		result = self.markerCounter.Add(1)
+	}
+	return result
 }
 
 func (self *RouterMessaging) RouterConnected(r *Router) {
@@ -69,6 +100,12 @@ func (self *RouterMessaging) RouterDeleted(routerId string) {
 	self.routerChanged(routerId, false)
 }
 
+func (self *RouterMessaging) TerminatorCreated(terminator *db.Terminator) {
+	self.queueEvent(&terminatorCreatedEvent{
+		terminator: terminator,
+	})
+}
+
 func (self *RouterMessaging) routerChanged(routerId string, connected bool) {
 	self.queueEvent(&routerChangedEvent{
 		routerId:  routerId,
@@ -106,30 +143,52 @@ func (self *RouterMessaging) run() {
 			}
 		}
 
-		if len(self.routers) > 0 {
+		if len(self.routerUpdates) > 0 {
 			self.syncStates()
 		}
+
+		if len(self.terminatorValidations) > 0 {
+			self.sendTerminatorValidationRequests()
+		}
 	}
 }
 
 func (self *RouterMessaging) getRouterStates(routerId string) *routerUpdates {
-	result, found := self.routers[routerId]
+	result, found := self.routerUpdates[routerId]
 	if !found {
 		result = &routerUpdates{
 			changedRouters: map[string]struct{}{},
 		}
-		self.routers[routerId] = result
+		self.routerUpdates[routerId] = result
+	}
+	return result
+}
+
+func (self *RouterMessaging) getTerminatorValidations(routerId string) *terminatorValidations {
+	result, found := self.terminatorValidations[routerId]
+	if !found {
+		result = &terminatorValidations{
+			terminators: map[string]terminatorInfo{},
+		}
+		self.terminatorValidations[routerId] = result
 	}
 	return result
 }
 
 func (self *RouterMessaging) syncStates() {
-	for k, v := range self.routers {
+	for k, v := range self.routerUpdates {
 		notifyRouterId := k
 		updates := v
 		changes := &ctrl_pb.PeerStateChanges{}
 		notifyRouter := self.managers.Routers.getConnected(notifyRouterId)
 		if notifyRouter == nil {
+			// if the router disconnected, we're going to sync everything anyway, so clear anything pending here
+			delete(self.routerUpdates, k)
+			continue
+		}
+
+		if v.sendInProgress.Load() {
+
 			continue
 		}
 
@@ -165,7 +224,10 @@ func (self *RouterMessaging) syncStates() {
 			}
 		}
 
-		updates.sendInProgress.Store(true)
+		if !updates.sendInProgress.CompareAndSwap(false, true) {
+			continue
+		}
+
 		currentStatesVersion := updates.version
 		queueErr := self.routerCommPool.QueueOrError(func() {
 			ch := notifyRouter.Control
@@ -179,7 +241,7 @@ func (self *RouterMessaging) syncStates() {
 				success = false
 			}
 
-			self.queueEvent(&routerSendDone{
+			self.queueEvent(&routerPeerChangesSendDone{
 				routerId: notifyRouter.Id,
 				version:  currentStatesVersion,
 				success:  success,
@@ -193,13 +255,129 @@ func (self *RouterMessaging) syncStates() {
 	}
 }
 
+func (self *RouterMessaging) sendTerminatorValidationRequests() {
+	for routerId, updates := range self.terminatorValidations {
+		self.sendTerminatorValidationRequest(routerId, updates)
+	}
+}
+
+func (self *RouterMessaging) sendTerminatorValidationRequest(routerId string, updates *terminatorValidations) {
+	notifyRouter := self.managers.Routers.getConnected(routerId)
+	if notifyRouter == nil {
+		// if the router disconnected, we're going to sync everything anyway, so clear anything pending here
+		delete(self.terminatorValidations, routerId)
+		return
+	}
+
+	if updates.checkInProgress.Load() {
+		if time.Since(updates.lastSend) > 3*time.Minute {
+			updates.checkInProgress.Store(false)
+		} else {
+			return
+		}
+	}
+
+	var terminators []*ctrl_pb.Terminator
+
+	for _, terminator := range updates.terminators {
+		if time.Since(terminator.GetCreatedAt()) > 5*time.Second {
+			pfxlog.Logger().WithField("terminatorId", terminator.GetId()).Info("queuing validate of terminator")
+			terminators = append(terminators, &ctrl_pb.Terminator{
+				Id:      terminator.GetId(),
+				Binding: terminator.GetBinding(),
+				Address: terminator.GetAddress(),
+				Marker:  terminator.marker,
+			})
+		}
+	}
+
+	if len(terminators) == 0 || !updates.checkInProgress.CompareAndSwap(false, true) {
+		return
+	}
+
+	req := &ctrl_pb.ValidateTerminatorsV2Request{
+		Terminators: terminators,
+		FixInvalid:  true,
+	}
+
+	queueErr := self.routerCommPool.QueueOrError(func() {
+		ch := notifyRouter.Control
+		if ch == nil {
+			return
+		}
+
+		if self.managers.Dispatcher.IsLeaderOrLeaderless() {
+			if err := protobufs.MarshalTyped(req).WithTimeout(time.Second * 1).SendAndWaitForWire(ch); err != nil {
+				pfxlog.Logger().WithError(err).WithField("routerId", notifyRouter.Id).Error("failed to send validate terminators request to router")
+			}
+		}
+	})
+
+	if queueErr != nil {
+		updates.checkInProgress.Store(false)
+	} else {
+		updates.lastSend = time.Now()
+	}
+}
+
+func (self *RouterMessaging) NewValidationResponseHandler(n *Network, r *Router) channel.ReceiveHandlerF {
+	return func(m *channel.Message, ch channel.Channel) {
+		log := pfxlog.Logger().WithField("routerId", r.Id)
+		resp := &ctrl_pb.ValidateTerminatorsV2Response{}
+		if err := protobufs.TypedResponse(resp).Unmarshall(m, nil); err != nil {
+			log.WithError(err).Error("unable to unmarshall validate terminators v2 response")
+			return
+		}
+
+		changeCtx := change.NewControlChannelChange(r.Id, r.Name, "fabric.validate.terminator", ch)
+
+		handler := &terminatorValidationRespReceived{
+			router:    r,
+			changeCtx: changeCtx,
+			resp:      resp,
+		}
+		handler.DeleteInvalid(n)
+		self.queueEvent(handler)
+	}
+}
+
+func (self *RouterMessaging) ValidateRouterTerminators(terminators []*Terminator) {
+	self.queueEvent(&validateTerminators{
+		terminators: terminators,
+	})
+}
+
+func (self *RouterMessaging) Inspect() (*inspect.RouterMessagingState, error) {
+	evt := &routerMessagingInspectEvent{
+		resultC: make(chan *inspect.RouterMessagingState, 1),
+	}
+
+	timeout := time.After(time.Second)
+
+	select {
+	case self.eventsC <- evt:
+	case <-timeout:
+		return nil, errors.New("timed out waiting to queue inspect event to router messaging")
+	}
+
+	select {
+	case result := <-evt.resultC:
+		return result, nil
+	case <-timeout:
+		return nil, errors.New("timed out waiting for inspect result from router messaging")
+	}
+}
+
 type routerChangedEvent struct {
 	routerId  string
 	connected bool
 }
 
 func (self *routerChangedEvent) handle(c *RouterMessaging) {
-	log.Infof("calculating router updates for router %v, connected=%v", self.routerId, self.connected)
+	pfxlog.Logger().WithField("routerId", self.routerId).
+		WithField("connected", self.connected).
+		Info("calculating router updates for router")
+
 	routers := c.managers.Routers.allConnected()
 
 	var sourceRouterState *routerUpdates
@@ -218,18 +396,158 @@ func (self *routerChangedEvent) handle(c *RouterMessaging) {
 	}
 }
 
-type routerSendDone struct {
+type terminatorCreatedEvent struct {
+	terminator *db.Terminator
+}
+
+func (self *terminatorCreatedEvent) handle(c *RouterMessaging) {
+	routerStates := c.getTerminatorValidations(self.terminator.Router)
+	routerStates.terminators[self.terminator.Id] = terminatorInfo{
+		Terminator: self.terminator,
+		marker:     c.getNextMarker(),
+	}
+}
+
+type routerPeerChangesSendDone struct {
 	routerId string
 	version  uint32
 	success  bool
 	states   *routerUpdates
 }
 
-func (self *routerSendDone) handle(c *RouterMessaging) {
-	if states, ok := c.routers[self.routerId]; ok {
+func (self *routerPeerChangesSendDone) handle(c *RouterMessaging) {
+	defer self.states.sendInProgress.Store(false)
+
+	if states, ok := c.routerUpdates[self.routerId]; ok {
 		if self.success && self.version == states.version {
-			delete(c.routers, self.routerId)
+			delete(c.routerUpdates, self.routerId)
+		}
+	}
+}
+
+type validateTerminators struct {
+	terminators []*Terminator
+}
+
+func (self *validateTerminators) handle(c *RouterMessaging) {
+	var currentRouterId string
+	var validations *terminatorValidations
+
+	routers := map[string]*terminatorValidations{}
+
+	for _, terminator := range self.terminators {
+		if terminator.Router != currentRouterId || validations == nil {
+			validations = c.getTerminatorValidations(terminator.Router)
+			currentRouterId = terminator.Router
+			routers[currentRouterId] = validations
+		}
+		validations.terminators[terminator.Id] = terminatorInfo{
+			Terminator: terminator,
+			marker:     c.getNextMarker(),
+		}
+	}
+}
+
+type terminatorValidationRespReceived struct {
+	router    *Router
+	changeCtx *change.Context
+	resp      *ctrl_pb.ValidateTerminatorsV2Response
+	success   bool
+}
+
+func (self *terminatorValidationRespReceived) DeleteInvalid(n *Network) {
+	log := pfxlog.Logger().WithField("routerId", self.router.Id)
+
+	var toDelete []string
+	for terminatorId, state := range self.resp.States {
+		if !state.Valid {
+			toDelete = append(toDelete, terminatorId)
+			log.WithField("terminatorId", terminatorId).
+				WithField("reason", state.Reason.String()).
+				Info("queuing terminator for delete")
 		}
 	}
-	self.states.sendInProgress.Store(false)
+
+	if len(toDelete) > 0 {
+		if err := n.Managers.Terminators.DeleteBatch(toDelete, self.changeCtx); err != nil {
+			for _, terminatorId := range toDelete {
+				log.WithField("terminatorId", terminatorId).
+					WithError(err).
+					Info("batch delete failed")
+			}
+		} else {
+			self.success = true
+		}
+	}
+}
+
+func (self *terminatorValidationRespReceived) handle(c *RouterMessaging) {
+	states := c.getTerminatorValidations(self.router.Id)
+	defer states.checkInProgress.Store(false)
+
+	for terminatorId, state := range self.resp.States {
+		if terminator, ok := states.terminators[terminatorId]; ok {
+			if (state.Valid && (state.Marker == 0 || state.Marker == terminator.marker)) || self.success {
+				delete(states.terminators, terminatorId)
+			}
+		}
+	}
+
+	if len(states.terminators) == 0 {
+		delete(c.terminatorValidations, self.router.Id)
+	}
+}
+
+type routerMessagingInspectEvent struct {
+	resultC chan *inspect.RouterMessagingState
+}
+
+func (self *routerMessagingInspectEvent) handle(c *RouterMessaging) {
+	result := &inspect.RouterMessagingState{}
+
+	getRouterName := func(routerId string) string {
+		if router, _ := c.managers.Routers.Read(routerId); router != nil {
+			return router.Name
+		}
+		return "<unknown>"
+	}
+
+	for routerId, updates := range c.routerUpdates {
+		u := &inspect.RouterUpdates{
+			Router: inspect.RouterInfo{
+				Id:   routerId,
+				Name: getRouterName(routerId),
+			},
+			Version: updates.version,
+		}
+		for updatedRouterId := range updates.changedRouters {
+			u.ChangedRouters = append(u.ChangedRouters, inspect.RouterInfo{
+				Id:   updatedRouterId,
+				Name: getRouterName(updatedRouterId),
+			})
+		}
+		result.RouterUpdates = append(result.RouterUpdates, u)
+	}
+
+	for routerId, pendingValidations := range c.terminatorValidations {
+		v := &inspect.TerminatorValidations{
+			Router: inspect.RouterInfo{
+				Id:   routerId,
+				Name: getRouterName(routerId),
+			},
+			CheckInProgress: pendingValidations.checkInProgress.Load(),
+			LastSend:        pendingValidations.lastSend.Format("2006-01-02 15:04:05"),
+		}
+
+		for terminatorId := range pendingValidations.terminators {
+			v.Terminators = append(v.Terminators, terminatorId)
+		}
+
+		result.TerminatorValidations = append(result.TerminatorValidations, v)
+	}
+
+	select {
+	case self.resultC <- result:
+	default:
+	}
 }
diff --git a/controller/raft/member.go b/controller/raft/member.go
index 532e70049..6ffcb0b1e 100644
--- a/controller/raft/member.go
+++ b/controller/raft/member.go
@@ -28,12 +28,12 @@ import (
 )
 
 type Member struct {
-	Id        string
-	Addr      string
-	Voter     bool
-	Leader    bool
-	Version   string
-	Connected bool
+	Id        string `json:"id"`
+	Addr      string `json:"addr"`
+	Voter     bool   `json:"isVoter"`
+	Leader    bool   `json:"isLeader"`
+	Version   string `json:"version"`
+	Connected bool   `json:"isConnected"`
 }
 
 func (self *Controller) ListMembers() ([]*Member, error) {
diff --git a/controller/raft/raft.go b/controller/raft/raft.go
index 6eb939584..e0d98a961 100644
--- a/controller/raft/raft.go
+++ b/controller/raft/raft.go
@@ -247,6 +247,10 @@ func (self *Controller) GetMesh() mesh.Mesh {
 	return self.Mesh
 }
 
+func (self *Controller) GetRateLimiter() command.RateLimiter {
+	return self.commandRateLimiter
+}
+
 func (self *Controller) ConfigureMeshHandlers(bindHandler channel.BindHandler) {
 	self.Mesh.Init(bindHandler)
 }
diff --git a/go.mod b/go.mod
index e1742866b..4586eedd4 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/go-openapi/loads v0.21.5
 	github.com/go-openapi/runtime v0.27.1
 	github.com/go-openapi/spec v0.20.14
-	github.com/go-openapi/strfmt v0.22.0
+	github.com/go-openapi/strfmt v0.22.1
 	github.com/go-openapi/swag v0.22.9
 	github.com/go-openapi/validate v0.23.0
 	github.com/go-resty/resty/v2 v2.11.0
@@ -35,7 +35,7 @@ require (
 	github.com/gorilla/websocket v1.5.1
 	github.com/hashicorp/go-hclog v1.6.2
 	github.com/hashicorp/golang-lru/v2 v2.0.7
-	github.com/hashicorp/raft v1.6.0
+	github.com/hashicorp/raft v1.6.1
 	github.com/hashicorp/raft-boltdb v0.0.0-20220329195025-15018e9b97e0
 	github.com/jedib0t/go-pretty/v6 v6.5.4
 	github.com/jessevdk/go-flags v1.5.0
@@ -49,7 +49,7 @@ require (
 	github.com/natefinch/lumberjack v2.0.0+incompatible
 	github.com/openziti/agent v1.0.16
 	github.com/openziti/channel/v2 v2.0.119
-	github.com/openziti/edge-api v0.26.11
+	github.com/openziti/edge-api v0.26.12
 	github.com/openziti/foundation/v2 v2.0.37
 	github.com/openziti/identity v1.0.70
 	github.com/openziti/jwks v1.0.3
@@ -77,12 +77,12 @@ require (
 	github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125
 	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/zitadel/oidc/v2 v2.12.0
-	go.etcd.io/bbolt v1.3.8
+	go.etcd.io/bbolt v1.3.9
 	go4.org v0.0.0-20180809161055-417644f6feb5
-	golang.org/x/crypto v0.19.0
+	golang.org/x/crypto v0.21.0
 	golang.org/x/net v0.21.0
 	golang.org/x/sync v0.6.0
-	golang.org/x/sys v0.17.0
+	golang.org/x/sys v0.18.0
 	golang.org/x/text v0.14.0
 	google.golang.org/protobuf v1.32.0
 	gopkg.in/AlecAivazis/survey.v1 v1.8.7
@@ -112,7 +112,7 @@ require (
 	github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 // indirect
 	github.com/felixge/httpsnoop v1.0.3 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
-	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
+	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/go-logr/logr v1.4.1 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
@@ -176,16 +176,16 @@ require (
 	github.com/yusufpapurcu/wmi v1.2.3 // indirect
 	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
-	go.opentelemetry.io/otel v1.23.1 // indirect
-	go.opentelemetry.io/otel/metric v1.23.1 // indirect
-	go.opentelemetry.io/otel/trace v1.23.1 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
 	golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
 	golang.org/x/image v0.13.0 // indirect
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/oauth2 v0.16.0 // indirect
-	golang.org/x/term v0.17.0 // indirect
+	golang.org/x/term v0.18.0 // indirect
 	golang.org/x/tools v0.17.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index cc2e357f8..330007943 100644
--- a/go.sum
+++ b/go.sum
@@ -204,8 +204,8 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
-github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
+github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -232,8 +232,8 @@ github.com/go-openapi/runtime v0.27.1 h1:ae53yaOoh+fx/X5Eaq8cRmavHgDma65XPZuvBqv
 github.com/go-openapi/runtime v0.27.1/go.mod h1:fijeJEiEclyS8BRurYE1DE5TLb9/KZl6eAdbzjsrlLU=
 github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
 github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
-github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI=
-github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
+github.com/go-openapi/strfmt v0.22.1 h1:5Ky8cybT4576C6Ffc+8gYji/wRXCo6Ozm8RaWjPI6jc=
+github.com/go-openapi/strfmt v0.22.1/go.mod h1:OfVoytIXJasDkkGvkb1Cceb3BPyMOwk1FgmyyEw7NYg=
 github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
 github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
 github.com/go-openapi/validate v0.23.0 h1:2l7PJLzCis4YUGEoW6eoQw3WhyM65WSIcjX6SQnlfDw=
@@ -386,8 +386,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM=
-github.com/hashicorp/raft v1.6.0 h1:tkIAORZy2GbJ2Trp5eUSggLXDPOJLXC+JJLNMMqtgtM=
-github.com/hashicorp/raft v1.6.0/go.mod h1:Xil5pDgeGwRWuX4uPUmwa+7Vagg4N804dz6mhNi6S7o=
+github.com/hashicorp/raft v1.6.1 h1:v/jm5fcYHvVkL0akByAp+IDdDSzCNCGhdO6VdB56HIM=
+github.com/hashicorp/raft v1.6.1/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0=
 github.com/hashicorp/raft-boltdb v0.0.0-20220329195025-15018e9b97e0 h1:CO8dBMLH6dvE1jTn/30ZZw3iuPsNfajshWoJTnVc5cc=
 github.com/hashicorp/raft-boltdb v0.0.0-20220329195025-15018e9b97e0/go.mod h1:nTakvJ4XYq45UXtn0DbwR4aU9ZdjlnIenpbs6Cd+FM0=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
@@ -568,8 +568,8 @@ github.com/openziti/channel/v2 v2.0.119 h1:stfSrnDqoTi78LMvQA3+NSivHjQnRrYKrgij5
 github.com/openziti/channel/v2 v2.0.119/go.mod h1:lSRJwqmbkE34DgXYEmUhVCzwcQcx65vZGE8nuBNK458=
 github.com/openziti/dilithium v0.3.3 h1:PLgQ6PMNLSTzCFbX/h98cmudgz/cU6TmjdSv5NAPD8k=
 github.com/openziti/dilithium v0.3.3/go.mod h1:vsCjI2AU/hon9e+dLhUFbCNGesJDj2ASgkySOcpmvjo=
-github.com/openziti/edge-api v0.26.11 h1:qINsfGpPBTnbuDrOq+qcMZuBdlXosqvHX7sQhLA+cM4=
-github.com/openziti/edge-api v0.26.11/go.mod h1:30SiUmR+9gOBi9HUZgXLpCO2nNCbNFVx2jwXV2Dh4Og=
+github.com/openziti/edge-api v0.26.12 h1:5VRz0cWtfQq2rhSA7Ne6amM7YNI6pQGRfNgbKt0g6kQ=
+github.com/openziti/edge-api v0.26.12/go.mod h1:tKZRUFDB9zM5J1zBS0ok2r40OhJqWykZaU9HSBQgr8w=
 github.com/openziti/foundation/v2 v2.0.37 h1:7pa4vWrlwllEoLXaK2rx91AffLQJ8k5pvc92oWANavA=
 github.com/openziti/foundation/v2 v2.0.37/go.mod h1:2NxzCnJbMw35U9RrFcdEaiXdxIMfBHOUNPngpyhvKeY=
 github.com/openziti/identity v1.0.70 h1:JNwtJHmIS0DcXookm2xuXyh4z92T1O21GQvuO8PmHWs=
@@ -797,8 +797,8 @@ github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
 github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4=
 github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
-go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@@ -814,14 +814,14 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
-go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
-go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
-go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
 go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE=
 go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ=
-go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
-go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
@@ -849,7 +849,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -858,8 +857,9 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1072,16 +1072,18 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/router/config.go b/router/config.go
index 29340436a..c9e09358d 100644
--- a/router/config.go
+++ b/router/config.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"fmt"
 	"github.com/openziti/transport/v2/tls"
+	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/router/env"
 	"io"
 	"os"
@@ -61,6 +62,23 @@ const (
 
 	// CtrlEndpointBindMapKey is the string key for the ctrl.bind section
 	CtrlEndpointBindMapKey = "bind"
+
+	// CtrlRateLimiterMinSizeValue is the minimum size that can be configured for the control channel rate limiter
+	// window range
+	CtrlRateLimiterMinSizeValue = 5
+
+	// CtrlRateLimiterMaxSizeValue is the maximum size that can be configured for the control channel rate limiter
+	// window range
+	CtrlRateLimiterMaxSizeValue = 1000
+
+	// CtrlRateLimiterMetricOutstandingCount is the name of the metric tracking how many tasks are in process
+	CtrlRateLimiterMetricOutstandingCount = "ctrl.limiter.in_process"
+
+	// CtrlRateLimiterMetricCurrentWindowSize is the name of the metric tracking the current window size
+	CtrlRateLimiterMetricCurrentWindowSize = "ctrl.limiter.window_size"
+
+	// CtrlRateLimiterMetricWorkTimer is the name of the metric tracking how long successful tasks are taking to complete
+	CtrlRateLimiterMetricWorkTimer = "ctrl.limiter.work_timer"
 )
 
 // internalConfigKeys is used to distinguish internally defined configuration vs file configuration
@@ -115,6 +133,7 @@ type Config struct {
 		DataDir               string
 		Heartbeats            env.HeartbeatOptions
 		StartupTimeout        time.Duration
+		RateLimit             command.AdaptiveRateLimiterConfig
 	}
 	Link struct {
 		Listeners  []map[interface{}]interface{}
@@ -527,6 +546,9 @@ func LoadConfig(path string) (*Config, error) {
 			} else {
 				cfg.Ctrl.DataDir = filepath.Dir(cfg.path)
 			}
+			if err = cfg.loadCtrlRateLimiterConfig(submap); err != nil {
+				return nil, err
+			}
 		}
 	}
 
@@ -767,6 +789,43 @@ func LoadConfig(path string) (*Config, error) {
 	return cfg, nil
 }
 
+func (c *Config) loadCtrlRateLimiterConfig(cfgmap map[interface{}]interface{}) error {
+	rateLimitConfig := &c.Ctrl.RateLimit
+	rateLimitConfig.SetDefaults()
+	rateLimitConfig.QueueSizeMetric = CtrlRateLimiterMetricOutstandingCount
+	rateLimitConfig.WindowSizeMetric = CtrlRateLimiterMetricCurrentWindowSize
+	rateLimitConfig.WorkTimerMetric = CtrlRateLimiterMetricWorkTimer
+
+	if value, found := cfgmap["rateLimiter"]; found {
+		if submap, ok := value.(map[interface{}]interface{}); ok {
+			if err := command.LoadAdaptiveRateLimiterConfig(rateLimitConfig, submap); err != nil {
+				return err
+			}
+			if rateLimitConfig.MaxSize < CtrlRateLimiterMinSizeValue {
+				return errors.Errorf("invalid value %v for ctrl.rateLimiter.maxSize, must be at least %v",
+					rateLimitConfig.MaxSize, CtrlRateLimiterMinSizeValue)
+			}
+			if rateLimitConfig.MaxSize > CtrlRateLimiterMaxSizeValue {
+				return errors.Errorf("invalid value %v for ctrl.rateLimiter.maxSize, must be at most %v",
+					rateLimitConfig.MaxSize, CtrlRateLimiterMaxSizeValue)
+			}
+
+			if rateLimitConfig.MinSize < CtrlRateLimiterMinSizeValue {
+				return errors.Errorf("invalid value %v for ctrl.rateLimiter.minSize, must be at least %v",
+					rateLimitConfig.MinSize, CtrlRateLimiterMinSizeValue)
+			}
+			if rateLimitConfig.MinSize > CtrlRateLimiterMaxSizeValue {
+				return errors.Errorf("invalid value %v for ctrl.rateLimiter.minSize, must be at most %v",
+					rateLimitConfig.MinSize, CtrlRateLimiterMaxSizeValue)
+			}
+		} else {
+			return errors.Errorf("invalid type for ctrl.rateLimiter, should be map instead of %T", value)
+		}
+	}
+
+	return nil
+}
+
 func LoadIdentityConfigFromMap(cfgmap map[interface{}]interface{}) (*identity.Config, error) {
 	if value, found := cfgmap["identity"]; found {
 		subMap := value.(map[interface{}]interface{})
diff --git a/router/env/env.go b/router/env/env.go
index 2dc39f18b..0fbe75533 100644
--- a/router/env/env.go
+++ b/router/env/env.go
@@ -22,6 +22,7 @@ import (
 	"github.com/openziti/foundation/v2/versions"
 	"github.com/openziti/identity"
 	"github.com/openziti/metrics"
+	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/router/xgress"
 	"github.com/openziti/ziti/router/xlink"
 )
@@ -39,5 +40,6 @@ type RouterEnv interface {
 	RenderJsonConfig() (string, error)
 	GetHeartbeatOptions() HeartbeatOptions
 	GetRateLimiterPool() goroutines.Pool
+	GetCtrlRateLimiter() command.AdaptiveRateLimitTracker
 	GetVersionInfo() versions.VersionProvider
 }
diff --git a/router/fabric/manager.go b/router/fabric/manager.go
index 5a6888e83..09b9febba 100644
--- a/router/fabric/manager.go
+++ b/router/fabric/manager.go
@@ -55,6 +55,7 @@ type StateManager interface {
 	RemoveEdgeSession(token string)
 	AddEdgeSessionRemovedListener(token string, callBack func(token string)) RemoveListener
 	WasSessionRecentlyRemoved(token string) bool
+	MarkSessionRecentlyRemoved(token string)
 
 	//ApiSessions
 	GetApiSession(token string) *ApiSession
@@ -289,6 +290,10 @@ func (sm *StateManagerImpl) WasSessionRecentlyRemoved(token string) bool {
 	return sm.recentlyRemovedSessions.Has(token)
 }
 
+func (sm *StateManagerImpl) MarkSessionRecentlyRemoved(token string) {
+	sm.recentlyRemovedSessions.Set(token, time.Now())
+}
+
 func (sm *StateManagerImpl) AddEdgeSessionRemovedListener(token string, callBack func(token string)) RemoveListener {
 	if sm.recentlyRemovedSessions.Has(token) {
 		go callBack(token) // callback can be long process with network traffic. Don't block event processing
@@ -444,7 +449,7 @@ func (sm *StateManagerImpl) flushRecentlyRemoved() {
 	sm.recentlyRemovedSessions.IterCb(func(key string, t time.Time) {
 		remove := false
 
-		if now.Sub(t) >= time.Minute {
+		if now.Sub(t) >= 5*time.Minute {
 			remove = true
 		}
 
diff --git a/router/handler_ctrl/bind.go b/router/handler_ctrl/bind.go
index 1a2c1de74..8846b4eb2 100644
--- a/router/handler_ctrl/bind.go
+++ b/router/handler_ctrl/bind.go
@@ -32,10 +32,11 @@ import (
 )
 
 type bindHandler struct {
-	env                env.RouterEnv
-	forwarder          *forwarder.Forwarder
-	xgDialerPool       goroutines.Pool
-	ctrlAddressUpdater CtrlAddressUpdater
+	env                      env.RouterEnv
+	forwarder                *forwarder.Forwarder
+	xgDialerPool             goroutines.Pool
+	terminatorValidationPool goroutines.Pool
+	ctrlAddressUpdater       CtrlAddressUpdater
 }
 
 func NewBindHandler(routerEnv env.RouterEnv, forwarder *forwarder.Forwarder, ctrlAddressUpdater CtrlAddressUpdater) (channel.BindHandler, error) {
@@ -57,11 +58,30 @@ func NewBindHandler(routerEnv env.RouterEnv, forwarder *forwarder.Forwarder, ctr
 		return nil, errors.Wrap(err, "error creating xgress route handler pool")
 	}
 
+	terminatorValidatorPoolConfig := goroutines.PoolConfig{
+		QueueSize:   uint32(1),
+		MinWorkers:  0,
+		MaxWorkers:  uint32(50),
+		IdleTime:    30 * time.Second,
+		CloseNotify: routerEnv.GetCloseNotify(),
+		PanicHandler: func(err interface{}) {
+			pfxlog.Logger().WithField(logrus.ErrorKey, err).WithField("backtrace", string(debug.Stack())).Error("panic during terminator validation operation")
+		},
+	}
+
+	metrics.ConfigureGoroutinesPoolMetrics(&terminatorValidatorPoolConfig, routerEnv.GetMetricsRegistry(), "pool.terminator_validation")
+
+	terminatorValidationPool, err := goroutines.NewPool(terminatorValidatorPoolConfig)
+	if err != nil {
+		return nil, errors.Wrap(err, "error creating terminator validation pool")
+	}
+
 	return &bindHandler{
-		env:                routerEnv,
-		forwarder:          forwarder,
-		xgDialerPool:       xgDialerPool,
-		ctrlAddressUpdater: ctrlAddressUpdater,
+		env:                      routerEnv,
+		forwarder:                forwarder,
+		xgDialerPool:             xgDialerPool,
+		terminatorValidationPool: terminatorValidationPool,
+		ctrlAddressUpdater:       ctrlAddressUpdater,
 	}, nil
 }
 
@@ -70,7 +90,7 @@ func (self *bindHandler) BindChannel(binding channel.Binding) error {
 	binding.AddTypedReceiveHandler(newDialHandler(self.env))
 	binding.AddTypedReceiveHandler(newRouteHandler(binding.GetChannel(), self.env, self.forwarder, self.xgDialerPool))
 	binding.AddTypedReceiveHandler(newValidateTerminatorsHandler(self.env))
-	binding.AddTypedReceiveHandler(newValidateTerminatorsV2Handler(self.env))
+	binding.AddTypedReceiveHandler(newValidateTerminatorsV2Handler(self.env, self.terminatorValidationPool))
 	binding.AddTypedReceiveHandler(newUnrouteHandler(self.forwarder))
 	binding.AddTypedReceiveHandler(newTraceHandler(self.env.GetRouterId(), self.forwarder.TraceController(), binding.GetChannel()))
 	binding.AddTypedReceiveHandler(newInspectHandler(self.env, self.forwarder))
diff --git a/router/handler_ctrl/inspect.go b/router/handler_ctrl/inspect.go
index b2e1705ba..7434c727e 100644
--- a/router/handler_ctrl/inspect.go
+++ b/router/handler_ctrl/inspect.go
@@ -18,12 +18,14 @@ package handler_ctrl
 
 import (
 	"encoding/json"
+	"fmt"
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/channel/v2"
+	"github.com/openziti/foundation/v2/debugz"
 	"github.com/openziti/ziti/common/pb/ctrl_pb"
 	"github.com/openziti/ziti/router/env"
 	"github.com/openziti/ziti/router/forwarder"
-	"github.com/openziti/foundation/v2/debugz"
+	"github.com/openziti/ziti/router/xgress"
 	"github.com/pkg/errors"
 	"google.golang.org/protobuf/proto"
 	"strings"
@@ -90,6 +92,29 @@ func (context *inspectRequestContext) processLocal() {
 			} else {
 				context.appendValue(requested, string(js))
 			}
+		} else if lc == "sdk-terminators" {
+			factory, _ := xgress.GlobalRegistry().Factory("edge")
+			if factory == nil {
+				context.appendError("no xgress factory configured for edge binding")
+				continue
+			}
+			dialer, err := factory.CreateDialer(context.handler.env.GetDialerCfg()["edge"])
+			if err != nil {
+				context.appendError(fmt.Sprintf("could not create edge dialer: (%s)", err.Error()))
+				continue
+			}
+			inspectable, ok := dialer.(xgress.Inspectable)
+			if !ok {
+				context.appendError("edge dialer is not of type Inspectable")
+				continue
+			}
+			result := inspectable.Inspect(lc, time.Second)
+			js, err := json.Marshal(result)
+			if err != nil {
+				context.appendError(errors.Wrap(err, "failed to marshal sdk terminators to json").Error())
+			} else {
+				context.appendValue(requested, string(js))
+			}
 		} else if strings.HasPrefix(lc, "circuit:") {
 			circuitId := requested[len("circuit:"):]
 			result := context.handler.fwd.InspectCircuit(circuitId, false)
diff --git a/router/handler_ctrl/validate_terminators_v2.go b/router/handler_ctrl/validate_terminators_v2.go
index 0a06666d2..4a0cd9021 100644
--- a/router/handler_ctrl/validate_terminators_v2.go
+++ b/router/handler_ctrl/validate_terminators_v2.go
@@ -20,19 +20,23 @@ import (
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/channel/v2"
 	"github.com/openziti/channel/v2/protobufs"
+	"github.com/openziti/foundation/v2/goroutines"
 	"github.com/openziti/ziti/common/pb/ctrl_pb"
 	"github.com/openziti/ziti/router/env"
 	"github.com/openziti/ziti/router/xgress"
 	"google.golang.org/protobuf/proto"
+	"time"
 )
 
 type validateTerminatorsV2Handler struct {
-	env env.RouterEnv
+	env  env.RouterEnv
+	pool goroutines.Pool
 }
 
-func newValidateTerminatorsV2Handler(env env.RouterEnv) *validateTerminatorsV2Handler {
+func newValidateTerminatorsV2Handler(env env.RouterEnv, pool goroutines.Pool) *validateTerminatorsV2Handler {
 	return &validateTerminatorsV2Handler{
-		env: env,
+		env:  env,
+		pool: pool,
 	}
 }
 
@@ -64,7 +68,11 @@ func (handler *validateTerminatorsV2Handler) validateTerminators(msg *channel.Me
 		States: map[string]*ctrl_pb.RouterTerminatorState{},
 	}
 
-	for _, terminator := range req.Terminators {
+	expected := 0
+	results := make(chan func(*ctrl_pb.ValidateTerminatorsV2Response), len(req.Terminators))
+
+	for _, val := range req.Terminators {
+		terminator := val
 		binding := terminator.Binding
 		dialer := dialers[binding]
 		if dialer == nil {
@@ -74,32 +82,42 @@ func (handler *validateTerminatorsV2Handler) validateTerminators(msg *channel.Me
 				}
 			}
 		}
-
+		log.WithField("terminatorId", terminator.Id).Debug("beginning terminator validation")
 		if dialer == nil {
 			response.States[terminator.Id] = &ctrl_pb.RouterTerminatorState{
 				Valid:  false,
 				Reason: ctrl_pb.TerminatorInvalidReason_UnknownBinding,
-			}
-		} else if inspectable, ok := dialer.(xgress.InspectableDialer); ok {
-			valid, state := inspectable.InspectTerminator(terminator.Id, terminator.Address, req.FixInvalid)
-			response.States[terminator.Id] = &ctrl_pb.RouterTerminatorState{
-				Valid:  valid,
-				Detail: state,
-				Reason: ctrl_pb.TerminatorInvalidReason_UnknownTerminator,
-			}
-		} else if !dialer.IsTerminatorValid(terminator.Id, terminator.Address) {
-			response.States[terminator.Id] = &ctrl_pb.RouterTerminatorState{
-				Valid:  false,
-				Reason: ctrl_pb.TerminatorInvalidReason_UnknownTerminator,
+				Marker: val.Marker,
 			}
 		} else {
-			response.States[terminator.Id] = &ctrl_pb.RouterTerminatorState{
-				Valid:  true,
-				Detail: "valid",
+			err := handler.pool.Queue(func() {
+				log.WithField("terminatorId", terminator.Id).Info("validating terminator")
+				result := handler.validateTerminator(dialer, terminator, req.FixInvalid)
+				results <- func(response *ctrl_pb.ValidateTerminatorsV2Response) {
+					response.States[terminator.Id] = result
+				}
+			})
+
+			if err != nil {
+				log.WithField("terminatorId", terminator.Id).WithError(err).Error("unable to queue inspect")
+			} else {
+				expected++
 			}
 		}
 	}
 
+	timeout := time.After(30 * time.Second)
+	timedOut := false
+	for i := 0; i < expected && !timedOut; i++ {
+		select {
+		case result := <-results:
+			result(response)
+		case <-timeout:
+			timedOut = true
+			log.Info("timed out waiting for terminator validations")
+		}
+	}
+
 	err := protobufs.MarshalTyped(response).
 		ReplyTo(msg).
 		WithTimeout(handler.env.GetNetworkControllers().DefaultRequestTimeout()).
@@ -109,3 +127,29 @@ func (handler *validateTerminatorsV2Handler) validateTerminators(msg *channel.Me
 		log.WithError(err).Error("failed to send validate terminators v2 response")
 	}
 }
+
+func (handler *validateTerminatorsV2Handler) validateTerminator(dialer xgress.Dialer, terminator *ctrl_pb.Terminator, fixInvalid bool) *ctrl_pb.RouterTerminatorState {
+	if inspectable, ok := dialer.(xgress.InspectableDialer); ok {
+		valid, state := inspectable.InspectTerminator(terminator.Id, terminator.Address, fixInvalid)
+		return &ctrl_pb.RouterTerminatorState{
+			Valid:  valid,
+			Detail: state,
+			Reason: ctrl_pb.TerminatorInvalidReason_UnknownTerminator,
+			Marker: terminator.Marker,
+		}
+	}
+
+	if !dialer.IsTerminatorValid(terminator.Id, terminator.Address) {
+		return &ctrl_pb.RouterTerminatorState{
+			Valid:  false,
+			Reason: ctrl_pb.TerminatorInvalidReason_UnknownTerminator,
+			Marker: terminator.Marker,
+		}
+	}
+
+	return &ctrl_pb.RouterTerminatorState{
+		Valid:  true,
+		Detail: "valid",
+		Marker: terminator.Marker,
+	}
+}
diff --git a/router/router.go b/router/router.go
index 593dc1e39..6c9ebcaa4 100644
--- a/router/router.go
+++ b/router/router.go
@@ -21,6 +21,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"github.com/openziti/ziti/controller/command"
 	"io/fs"
 	"os"
 	"path"
@@ -82,6 +83,7 @@ type Router struct {
 	xgressListeners []xgress.Listener
 	linkDialerPool  goroutines.Pool
 	rateLimiterPool goroutines.Pool
+	ctrlRateLimiter command.AdaptiveRateLimitTracker
 	metricsRegistry metrics.UsageRegistry
 	shutdownC       chan struct{}
 	shutdownDoneC   chan struct{}
@@ -191,6 +193,7 @@ func Create(config *Config, versionProvider versions.VersionProvider) *Router {
 		debugOperations:     map[byte]func(c *bufio.ReadWriter) error{},
 		xwebFactoryRegistry: xweb.NewRegistryMap(),
 		linkDialerPool:      linkDialerPool,
+		ctrlRateLimiter:     command.NewAdaptiveRateLimitTracker(config.Ctrl.RateLimit, metricsRegistry, closeNotify),
 	}
 
 	router.ctrls = env.NewNetworkControllers(config.Ctrl.DefaultRequestTimeout, router.connectToController, &config.Ctrl.Heartbeats)
@@ -379,6 +382,10 @@ func (self *Router) GetRateLimiterPool() goroutines.Pool {
 	return self.rateLimiterPool
 }
 
+func (self *Router) GetCtrlRateLimiter() command.AdaptiveRateLimitTracker {
+	return self.ctrlRateLimiter
+}
+
 func (self *Router) registerComponents() error {
 	self.xlinkFactories = make(map[string]xlink.Factory)
 	xlinkAccepter := newXlinkAccepter(self.forwarder)
diff --git a/router/router_test.go b/router/router_test.go
index d2fce62f7..53fdd30ab 100644
--- a/router/router_test.go
+++ b/router/router_test.go
@@ -1,6 +1,7 @@
 package router
 
 import (
+	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/router/env"
 	"github.com/stretchr/testify/require"
 	"os"
@@ -50,6 +51,7 @@ func Test_initializeCtrlEndpoints(t *testing.T) {
 				DataDir               string
 				Heartbeats            env.HeartbeatOptions
 				StartupTimeout        time.Duration
+				RateLimit             command.AdaptiveRateLimiterConfig
 			}{
 				DataDir:          tmpDir,
 				InitialEndpoints: []*UpdatableAddress{NewUpdatableAddress(addr)},
@@ -91,6 +93,7 @@ func Test_updateCtrlEndpoints(t *testing.T) {
 				DataDir               string
 				Heartbeats            env.HeartbeatOptions
 				StartupTimeout        time.Duration
+				RateLimit             command.AdaptiveRateLimiterConfig
 			}{
 				DataDir:          tmpDir,
 				InitialEndpoints: []*UpdatableAddress{NewUpdatableAddress(addr), NewUpdatableAddress(addr2)},
diff --git a/router/xgress/request.go b/router/xgress/request.go
index d0e1e59ab..6dbbe7c48 100644
--- a/router/xgress/request.go
+++ b/router/xgress/request.go
@@ -21,10 +21,10 @@ import (
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/channel/v2"
 	"github.com/openziti/channel/v2/protobufs"
-	"github.com/openziti/ziti/common/ctrl_msg"
-	"github.com/openziti/ziti/common/pb/ctrl_pb"
 	"github.com/openziti/identity"
 	"github.com/openziti/transport/v2"
+	"github.com/openziti/ziti/common/ctrl_msg"
+	"github.com/openziti/ziti/common/pb/ctrl_pb"
 	"github.com/pkg/errors"
 	"io"
 	"time"
diff --git a/router/xgress/xgress.go b/router/xgress/xgress.go
index b6ae4b170..d876e4855 100644
--- a/router/xgress/xgress.go
+++ b/router/xgress/xgress.go
@@ -77,6 +77,10 @@ type InspectableDialer interface {
 	InspectTerminator(id string, destination string, fixInvalid bool) (bool, string)
 }
 
+type Inspectable interface {
+	Inspect(key string, timeout time.Duration) any
+}
+
 type Factory interface {
 	CreateListener(optionsData OptionsData) (Listener, error)
 	CreateDialer(optionsData OptionsData) (Dialer, error)
diff --git a/router/xgress_edge/dialer.go b/router/xgress_edge/dialer.go
index 13a31b6c2..c9839fec9 100644
--- a/router/xgress_edge/dialer.go
+++ b/router/xgress_edge/dialer.go
@@ -44,8 +44,9 @@ func (dialer *dialer) InspectTerminator(id string, destination string, fixInvali
 	terminatorAddress := strings.TrimPrefix(destination, "hosted:")
 	pfxlog.Logger().Debug("looking up hosted service conn")
 	terminator, found := dialer.factory.hostedServices.Get(terminatorAddress)
-	if found && terminator.terminatorId.Load() == id {
-		result, err := terminator.inspect(fixInvalid)
+	if found && terminator.terminatorId == id {
+		dialer.factory.hostedServices.markEstablished(terminator, "validation message received")
+		result, err := terminator.inspect(dialer.factory.hostedServices, fixInvalid, false)
 		if err != nil {
 			return true, err.Error()
 		}
@@ -187,3 +188,10 @@ func (dialer *dialer) Dial(params xgress.DialParams) (xt.PeerData, error) {
 		return nil, terminator.SendState(start)
 	}
 }
+
+func (dialer *dialer) Inspect(key string, timeout time.Duration) any {
+	if key == "sdk-terminators" {
+		return dialer.factory.hostedServices.Inspect(timeout)
+	}
+	return nil
+}
diff --git a/router/xgress_edge/fabric.go b/router/xgress_edge/fabric.go
index e1e35a981..861e6d657 100644
--- a/router/xgress_edge/fabric.go
+++ b/router/xgress_edge/fabric.go
@@ -17,17 +17,19 @@
 package xgress_edge
 
 import (
+	"fmt"
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/channel/v2"
 	"github.com/openziti/foundation/v2/concurrenz"
 	"github.com/openziti/sdk-golang/ziti/edge"
 	"github.com/openziti/ziti/common/pb/edge_ctrl_pb"
+	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/router/xgress"
 	"github.com/openziti/ziti/router/xgress_common"
 	"github.com/pkg/errors"
 	"io"
 	"math"
-	"math/rand"
+	"sync"
 	"sync/atomic"
 	"time"
 )
@@ -44,16 +46,13 @@ var headersFromFabric = map[uint8]int32{
 type terminatorState int
 
 const (
-	TerminatorStatePendingEstablishment terminatorState = 0
-	TerminatorStateEstablishing         terminatorState = 1
-	TerminatorStateEstablished          terminatorState = 2
-	TerminatorStateDeleting             terminatorState = 3
+	TerminatorStateEstablishing terminatorState = 1
+	TerminatorStateEstablished  terminatorState = 2
+	TerminatorStateDeleting     terminatorState = 3
 )
 
 func (self terminatorState) String() string {
 	switch self {
-	case TerminatorStatePendingEstablishment:
-		return "pending-establishment"
 	case TerminatorStateEstablishing:
 		return "establishing"
 	case TerminatorStateEstablished:
@@ -65,10 +64,23 @@ func (self terminatorState) String() string {
 	}
 }
 
+func (self terminatorState) IsWorkRequired() bool {
+	switch self {
+	case TerminatorStateEstablishing:
+		return true
+	case TerminatorStateDeleting:
+		return true
+	case TerminatorStateEstablished:
+		return false
+	default:
+		return false
+	}
+}
+
 type edgeTerminator struct {
 	edge.MsgChannel
 	edgeClientConn    *edgeClientConn
-	terminatorId      concurrenz.AtomicValue[string]
+	terminatorId      string
 	listenerId        string
 	token             string
 	instance          string
@@ -80,44 +92,113 @@ type edgeTerminator struct {
 	onClose           func()
 	v2                bool
 	state             concurrenz.AtomicValue[terminatorState]
-	postValidate      bool
-	nextAttempt       time.Time
-	retryDelay        time.Duration
-	establishActive   atomic.Bool
+	supportsInspect   bool
+	operationActive   atomic.Bool
 	createTime        time.Time
-	establishCallback func(ok bool, msg string)
+	lastAttempt       time.Time
+	establishCallback func(result edge_ctrl_pb.CreateTerminatorResult)
+	lock              sync.Mutex
+	rateLimitCallback command.RateLimitControl
 }
 
-func (self *edgeTerminator) calculateRetry(queueFailed bool) {
-	retryDelay := time.Second
+func (self *edgeTerminator) replace(other *edgeTerminator) {
+	other.lock.Lock()
+	terminatorId := other.terminatorId
+	state := other.state.Load()
+	rateLimitCallback := other.rateLimitCallback
+	operationActive := other.operationActive.Load()
+	createTime := other.createTime
+	lastAttempt := other.lastAttempt
+	other.lock.Unlock()
+
+	self.lock.Lock()
+	self.terminatorId = terminatorId
+	self.setState(state, "replacing existing terminator")
+	self.rateLimitCallback = rateLimitCallback
+	self.operationActive.Store(operationActive)
+	self.createTime = createTime
+	self.lastAttempt = lastAttempt
+	self.lock.Unlock()
+}
 
-	if !queueFailed {
-		if self.retryDelay < time.Second {
-			self.retryDelay = time.Second
-		} else {
-			self.retryDelay = self.retryDelay * 2
-			if self.retryDelay > 30*time.Second {
-				self.retryDelay = 30 * time.Second
-			}
-		}
-		retryDelay = self.retryDelay
+func (self *edgeTerminator) IsEstablishing() bool {
+	return self.state.Load() == TerminatorStateEstablishing
+}
+
+func (self *edgeTerminator) IsDeleting() bool {
+	return self.state.Load() == TerminatorStateDeleting
+}
+
+func (self *edgeTerminator) setState(state terminatorState, reason string) {
+	if oldState := self.state.Load(); oldState != state {
+		self.state.Store(state)
+		pfxlog.Logger().WithField("terminatorId", self.terminatorId).
+			WithField("oldState", oldState).
+			WithField("newState", state).
+			WithField("reason", reason).
+			Info("updated state")
 	}
+}
 
-	actualDelay := (float64(retryDelay.Milliseconds()) * 1.5) - float64(rand.Intn(int(retryDelay.Milliseconds())))
-	self.nextAttempt = time.Now().Add(time.Millisecond * time.Duration(actualDelay))
+func (self *edgeTerminator) updateState(oldState, newState terminatorState, reason string) bool {
+	log := pfxlog.Logger().WithField("terminatorId", self.terminatorId).
+		WithField("oldState", oldState).
+		WithField("newState", newState).
+		WithField("reason", reason)
+	success := self.state.CompareAndSwap(oldState, newState)
+	if success {
+		log.Info("updated state")
+	}
+	return success
 }
 
-func (self *edgeTerminator) inspect(fixInvalidTerminators bool) (*edge.InspectResult, error) {
-	msg := channel.NewMessage(edge.ContentTypeConnInspectRequest, nil)
-	msg.PutUint32Header(edge.ConnIdHeader, self.Id())
-	resp, err := msg.WithTimeout(10 * time.Second).SendForReply(self.Channel)
-	if err != nil {
-		return nil, errors.New("unable to check status with sdk client")
+func (self *edgeTerminator) inspect(registry *hostedServiceRegistry, fixInvalidTerminators bool, notifyCtrl bool) (*edge.InspectResult, error) {
+	result := &edge.InspectResult{
+		ConnId: self.MsgChannel.Id(),
+		Type:   edge.ConnTypeUnknown,
+		Detail: "channel closed",
 	}
-	result, err := edge.UnmarshalInspectResult(resp)
-	if result != nil && result.Type != edge.ConnTypeBind && fixInvalidTerminators {
-		self.close(true, true, "terminator invalid")
+
+	var err error
+	isInvalid := false
+	if !self.Channel.IsClosed() {
+		msg := channel.NewMessage(edge.ContentTypeConnInspectRequest, nil)
+		msg.PutUint32Header(edge.ConnIdHeader, self.Id())
+		resp, err := msg.WithTimeout(10 * time.Second).SendForReply(self.Channel)
+		if err != nil {
+			return nil, fmt.Errorf("unable to check status with sdk client: (%w)", err)
+		}
+
+		result, err = edge.UnmarshalInspectResult(resp)
+		if err != nil {
+			return nil, fmt.Errorf("unable to unmarshal inspect response from sdk client: (%w)", err)
+		}
+
+		isInvalid = result != nil && result.Type != edge.ConnTypeBind
+	} else {
+		isInvalid = true
+		err = errors.New("channel closed")
 	}
+
+	if isInvalid && fixInvalidTerminators {
+		removeResult := registry.handleSdkReturnedInvalid(self, notifyCtrl)
+		if removeResult.err != nil {
+			return nil, err
+		}
+		if removeResult.removed || !removeResult.existed {
+			return result, err
+		}
+		current, _ := registry.Get(self.terminatorId)
+		if current == nil {
+			return result, err
+		}
+
+		if current == self { // this shouldn't happen
+			return result, errors.New("wasn't able to remove, but is still current")
+		}
+		return current.inspect(registry, fixInvalidTerminators, notifyCtrl)
+	}
+
 	return result, err
 }
 
@@ -130,9 +211,9 @@ func (self *edgeTerminator) nextDialConnId() uint32 {
 	return nextId
 }
 
-func (self *edgeTerminator) close(notifySdk bool, notifyCtrl bool, reason string) {
+func (self *edgeTerminator) close(registry *hostedServiceRegistry, notifySdk bool, notifyCtrl bool, reason string) {
 	logger := pfxlog.Logger().
-		WithField("terminatorId", self.terminatorId.Load()).
+		WithField("terminatorId", self.terminatorId).
 		WithField("token", self.token).
 		WithField("reason", reason)
 
@@ -146,36 +227,19 @@ func (self *edgeTerminator) close(notifySdk bool, notifyCtrl bool, reason string
 	}
 
 	if self.v2 {
-		if terminatorId := self.terminatorId.Load(); terminatorId != "" {
-			if self.terminatorId.CompareAndSwap(terminatorId, "") {
-				logger.Debug("removing terminator on router")
-
-				self.state.Store(TerminatorStateDeleting)
-				self.edgeClientConn.listener.factory.hostedServices.Delete(terminatorId)
-
-				if notifyCtrl {
-					logger.Info("removing terminator on controller")
-					ctrlCh := self.edgeClientConn.listener.factory.ctrls.AnyCtrlChannel()
-					if ctrlCh == nil {
-						logger.Error("no controller available, unable to remove terminator")
-					} else if err := self.edgeClientConn.removeTerminator(ctrlCh, self.token, terminatorId); err != nil {
-						logger.WithError(err).Error("failed to remove terminator")
-					} else {
-						logger.Info("Successfully removed terminator")
-					}
-				}
-			} else {
-				logger.Warn("edge terminator closing, but no terminator id set, so can't remove on controller")
-			}
+		if notifyCtrl {
+			registry.queueRemoveTerminatorAsync(self, reason)
+		} else {
+			self.edgeClientConn.listener.factory.hostedServices.Remove(self, reason)
 		}
 	} else {
 		if notifyCtrl {
-			if terminatorId := self.terminatorId.Load(); terminatorId != "" {
+			if self.terminatorId != "" {
 				logger.Info("removing terminator on controller")
 				ctrlCh := self.edgeClientConn.listener.factory.ctrls.AnyCtrlChannel()
 				if ctrlCh == nil {
 					logger.Error("no controller available, unable to remove terminator")
-				} else if err := self.edgeClientConn.removeTerminator(ctrlCh, self.token, terminatorId); err != nil {
+				} else if err := self.edgeClientConn.removeTerminator(ctrlCh, self.token, self.terminatorId); err != nil {
 					logger.WithError(err).Error("failed to remove terminator")
 				} else {
 					logger.Info("successfully removed terminator")
@@ -185,7 +249,7 @@ func (self *edgeTerminator) close(notifySdk bool, notifyCtrl bool, reason string
 			}
 		}
 
-		logger.Debug("removing terminator on router")
+		logger.Info("terminator removed from router set")
 		self.edgeClientConn.listener.factory.hostedServices.Delete(self.token)
 	}
 
@@ -209,6 +273,20 @@ func (self *edgeTerminator) newConnection(connId uint32) (*edgeXgressConn, error
 	return result, nil
 }
 
+func (self *edgeTerminator) SetRateLimitCallback(control command.RateLimitControl) {
+	self.lock.Lock()
+	defer self.lock.Unlock()
+	self.rateLimitCallback = control
+}
+
+func (self *edgeTerminator) GetAndClearRateLimitCallback() command.RateLimitControl {
+	self.lock.Lock()
+	defer self.lock.Unlock()
+	result := self.rateLimitCallback
+	self.rateLimitCallback = nil
+	return result
+}
+
 type edgeXgressConn struct {
 	edge.MsgChannel
 	mux     edge.MsgMux
diff --git a/router/xgress_edge/hosted.go b/router/xgress_edge/hosted.go
index ab19dbe2f..ddf64f75c 100644
--- a/router/xgress_edge/hosted.go
+++ b/router/xgress_edge/hosted.go
@@ -17,39 +17,45 @@
 package xgress_edge
 
 import (
-	"container/heap"
 	"fmt"
 	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/channel/v2"
 	"github.com/openziti/channel/v2/protobufs"
-	"github.com/openziti/sdk-golang/ziti/edge"
+	"github.com/openziti/ziti/common/handler_common"
+	"github.com/openziti/ziti/common/inspect"
+	"github.com/openziti/ziti/common/pb/ctrl_pb"
 	"github.com/openziti/ziti/common/pb/edge_ctrl_pb"
+	"github.com/openziti/ziti/controller/apierror"
+	"github.com/openziti/ziti/controller/command"
 	routerEnv "github.com/openziti/ziti/router/env"
+	cmap "github.com/orcaman/concurrent-map/v2"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"google.golang.org/protobuf/proto"
-	"sync"
+	"sync/atomic"
 	"time"
 )
 
 func newHostedServicesRegistry(env routerEnv.RouterEnv) *hostedServiceRegistry {
 	result := &hostedServiceRegistry{
-		services:        sync.Map{},
-		events:          make(chan terminatorEvent),
-		env:             env,
-		retriesPending:  false,
-		terminatorQueue: &terminatorHeap{},
+		terminators:  cmap.New[*edgeTerminator](),
+		events:       make(chan terminatorEvent),
+		env:          env,
+		triggerEvalC: make(chan struct{}, 1),
+		establishSet: map[string]*edgeTerminator{},
+		deleteSet:    map[string]*edgeTerminator{},
 	}
 	go result.run()
 	return result
 }
 
 type hostedServiceRegistry struct {
-	services        sync.Map
-	events          chan terminatorEvent
-	env             routerEnv.RouterEnv
-	retriesPending  bool
-	terminatorQueue *terminatorHeap
+	terminators  cmap.ConcurrentMap[string, *edgeTerminator]
+	events       chan terminatorEvent
+	env          routerEnv.RouterEnv
+	establishSet map[string]*edgeTerminator
+	deleteSet    map[string]*edgeTerminator
+	triggerEvalC chan struct{}
 }
 
 type terminatorEvent interface {
@@ -57,67 +63,290 @@ type terminatorEvent interface {
 }
 
 func (self *hostedServiceRegistry) run() {
-	queueCheckTicker := time.NewTicker(100 * time.Millisecond)
-	defer queueCheckTicker.Stop()
-
-	longQueueCheckTicker := time.NewTicker(time.Second)
+	longQueueCheckTicker := time.NewTicker(time.Minute)
 	defer longQueueCheckTicker.Stop()
 
+	quickTick := time.NewTicker(10 * time.Millisecond)
+	defer quickTick.Stop()
+
 	for {
-		var retryChan <-chan time.Time
-		if self.retriesPending {
-			retryChan = queueCheckTicker.C
+		var rateLimitedTick <-chan time.Time
+		if self.env.GetCtrlRateLimiter().IsRateLimited() {
+			rateLimitedTick = quickTick.C
 		}
-
 		select {
 		case <-self.env.GetCloseNotify():
 			return
 		case event := <-self.events:
 			event.handle(self)
-		case <-retryChan:
-			self.evaluateLinkStateQueue()
 		case <-longQueueCheckTicker.C:
 			self.scanForRetries()
+		case <-self.triggerEvalC:
+		case <-rateLimitedTick:
+		}
+
+		// events should be quick to handle, so make sure we do all them before we
+		// try the establish/delete queues
+		allEventsHandled := false
+		for !allEventsHandled {
+			select {
+			case event := <-self.events:
+				event.handle(self)
+			default:
+				allEventsHandled = true
+			}
+		}
+
+		if !self.env.GetCtrlRateLimiter().IsRateLimited() {
+			self.evaluateEstablishQueue()
+		}
+
+		if !self.env.GetCtrlRateLimiter().IsRateLimited() {
+			self.evaluateDeleteQueue()
 		}
 	}
 }
 
-func (self *hostedServiceRegistry) evaluateLinkStateQueue() {
-	now := time.Now()
-	for len(*self.terminatorQueue) > 0 {
-		next := (*self.terminatorQueue)[0]
-		if now.Before(next.nextAttempt) {
+func (self *hostedServiceRegistry) triggerEvaluates() {
+	select {
+	case self.triggerEvalC <- struct{}{}:
+	default:
+	}
+}
+
+func (self *hostedServiceRegistry) evaluateEstablishQueue() {
+	for id, terminator := range self.establishSet {
+		dequeue := func() {
+			delete(self.establishSet, id)
+		}
+
+		log := logrus.
+			WithField("terminatorId", terminator.terminatorId).
+			WithField("state", terminator.state.Load()).
+			WithField("token", terminator.token)
+
+		if terminator.edgeClientConn.ch.IsClosed() {
+			self.Remove(terminator, "sdk connection is closed")
+			log.Infof("terminator sdk channel closed, not trying to establish")
+			dequeue()
+			continue
+		}
+
+		if terminator.terminatorId == "" {
+			log.Info("terminator has been closed, not trying to establish")
+			dequeue()
+			continue
+		}
+
+		if terminator.state.Load() != TerminatorStateEstablishing {
+			dequeue()
+			continue
+		}
+
+		label := fmt.Sprintf("establish terminator %s", terminator.terminatorId)
+		rateLimitCtrl, err := self.env.GetCtrlRateLimiter().RunRateLimited(label)
+		if err != nil {
+			log.Info("rate limiter hit, waiting for a slot to open")
 			return
 		}
-		heap.Pop(self.terminatorQueue)
-		self.evaluateTerminator(next)
+
+		if !terminator.operationActive.CompareAndSwap(false, true) && time.Since(terminator.lastAttempt) < 30*time.Second {
+			rateLimitCtrl.Failed()
+			continue
+		}
+
+		log.Info("queuing terminator to send create")
+
+		dequeue()
+		terminator.SetRateLimitCallback(rateLimitCtrl)
+		terminator.lastAttempt = time.Now()
+
+		if err = self.establishTerminator(terminator); err != nil {
+			log.WithError(err).Error("error establishing terminator")
+			self.queueEstablishTerminatorSync(terminator)
+			rateLimitCtrl.Failed()
+			return // if we had an error it's because the channel was busy or a controller wasn't available
+		}
 	}
-	self.retriesPending = false
 }
 
-type establishTerminatorEvent struct {
+func (self *hostedServiceRegistry) evaluateDeleteQueue() {
+	var deleteList []*edgeTerminator
+
+	for terminatorId, terminator := range self.deleteSet {
+		log := logrus.
+			WithField("terminatorId", terminator.terminatorId).
+			WithField("state", terminator.state.Load()).
+			WithField("token", terminator.token)
+
+		delete(self.deleteSet, terminatorId)
+
+		if terminator.state.Load() != TerminatorStateDeleting {
+			continue
+		}
+
+		if current, exists := self.terminators.Get(terminatorId); exists && current != terminator {
+			continue
+		}
+
+		if terminator.operationActive.Load() {
+			if time.Since(terminator.lastAttempt) > 30*time.Second {
+				terminator.operationActive.Store(false)
+			} else {
+				continue
+			}
+		}
+
+		log.Info("added terminator to batch delete")
+		deleteList = append(deleteList, terminator)
+		if len(deleteList) >= 50 {
+			if !self.RemoveTerminatorsRateLimited(deleteList) {
+				return
+			}
+			deleteList = nil
+		}
+	}
+
+	if len(deleteList) != 0 {
+		self.RemoveTerminatorsRateLimited(deleteList)
+	}
+}
+
+func (self *hostedServiceRegistry) RemoveTerminatorsRateLimited(terminators []*edgeTerminator) bool {
+	if self.env.GetCtrlRateLimiter().IsRateLimited() {
+		self.requeueForDeleteSync(terminators)
+		return false
+	}
+
+	for _, terminator := range terminators {
+		terminator.operationActive.Store(true)
+	}
+
+	err := self.env.GetRateLimiterPool().QueueOrError(func() {
+		rateLimitCtrl, err := self.env.GetCtrlRateLimiter().RunRateLimited("remove terminator batch")
+		if err != nil {
+			pfxlog.Logger().Debug("rate limiter hit, waiting for a slot to open before doing sdk terminator deletes")
+
+			for _, terminator := range terminators {
+				pfxlog.Logger().WithError(err).WithField("terminatorId", terminator.terminatorId).
+					Error("remove terminator failed")
+				self.requeueRemoveTerminatorAsync(terminator)
+			}
+			return
+		}
+
+		var terminatorIds []string
+		for _, terminator := range terminators {
+			terminatorIds = append(terminatorIds, terminator.terminatorId)
+		}
+
+		if err := self.RemoveTerminators(terminatorIds); err != nil {
+			if command.WasRateLimited(err) {
+				rateLimitCtrl.Backoff()
+			} else {
+				rateLimitCtrl.Failed()
+			}
+
+			for _, terminator := range terminators {
+				pfxlog.Logger().WithError(err).WithField("terminatorId", terminator.terminatorId).
+					Error("remove terminator failed")
+				self.requeueRemoveTerminatorAsync(terminator)
+			}
+		} else {
+			rateLimitCtrl.Success()
+			for _, terminator := range terminators {
+				pfxlog.Logger().WithField("terminatorId", terminator.terminatorId).
+					Info("remove terminator succeeded")
+				terminator.operationActive.Store(false)
+				if !self.Remove(terminator, "controller delete success") {
+					pfxlog.Logger().WithField("terminatorId", terminator.terminatorId).
+						Error("terminator was replaced after being put into deleting state?!")
+				}
+			}
+		}
+	})
+
+	if err != nil {
+		pfxlog.Logger().WithError(err).Error("unable to queue remove terminators operation")
+		self.requeueForDeleteSync(terminators)
+		return false
+	}
+
+	return true
+}
+
+func (self *hostedServiceRegistry) requeueForDeleteSync(terminators []*edgeTerminator) {
+	for _, terminator := range terminators {
+		existing, _ := self.terminators.Get(terminator.terminatorId)
+		if existing == nil || existing == terminator { // make sure we're still the current terminator
+			terminator.setState(TerminatorStateDeleting, "deleting")
+			terminator.operationActive.Store(false)
+			self.deleteSet[terminator.terminatorId] = terminator
+		}
+	}
+}
+
+func (self *hostedServiceRegistry) RemoveTerminators(terminatorIds []string) error {
+	log := pfxlog.Logger()
+	request := &ctrl_pb.RemoveTerminatorsRequest{
+		TerminatorIds: terminatorIds,
+	}
+
+	ctrls := self.env.GetNetworkControllers()
+	ctrlCh := ctrls.AnyValidCtrlChannel()
+	if ctrlCh == nil {
+		return errors.New("no controller available")
+	}
+
+	responseMsg, err := protobufs.MarshalTyped(request).WithTimeout(ctrls.DefaultRequestTimeout()).SendForReply(ctrlCh)
+	if err != nil {
+		return fmt.Errorf("failed to send RemoveTerminatorsRequest message (%w)", err)
+	}
+
+	if responseMsg.ContentType != channel.ContentTypeResultType {
+		return fmt.Errorf("failure deleting terminators (unexpected response content type: %d)", responseMsg.ContentType)
+	}
+
+	result := channel.UnmarshalResult(responseMsg)
+	if result.Success {
+		return nil
+	}
+
+	if handler_common.WasRateLimited(responseMsg) {
+		log.Errorf("failure removing terminators (%v)", result.Message)
+		return apierror.NewTooManyUpdatesError()
+	}
+
+	return fmt.Errorf("failure deleting terminators (%s)", result.Message)
+}
+
+type queueEstablishTerminator struct {
+	terminator *edgeTerminator
+}
+
+func (self *queueEstablishTerminator) handle(registry *hostedServiceRegistry) {
+	registry.queueEstablishTerminatorSync(self.terminator)
+}
+
+type requeueRemoveTerminator struct {
 	terminator *edgeTerminator
 }
 
-func (self *establishTerminatorEvent) handle(registry *hostedServiceRegistry) {
-	registry.evaluateTerminator(self.terminator)
+func (self *requeueRemoveTerminator) handle(registry *hostedServiceRegistry) {
+	registry.requeueRemoveTerminatorSync(self.terminator)
 }
 
-type calculateRetry struct {
-	terminator  *edgeTerminator
-	queueFailed bool
+type queueRemoveTerminator struct {
+	terminator *edgeTerminator
+	reason     string
 }
 
-func (self *calculateRetry) handle(registry *hostedServiceRegistry) {
-	self.terminator.calculateRetry(self.queueFailed)
-	registry.retriesPending = true
+func (self *queueRemoveTerminator) handle(registry *hostedServiceRegistry) {
+	registry.queueRemoveTerminatorSync(self.terminator, self.reason)
 }
 
 func (self *hostedServiceRegistry) EstablishTerminator(terminator *edgeTerminator) {
-	self.Put(terminator.terminatorId.Load(), terminator)
-	self.queue(&establishTerminatorEvent{
-		terminator: terminator,
-	})
+	self.queueEstablishTerminatorAsync(terminator)
 }
 
 func (self *hostedServiceRegistry) queue(event terminatorEvent) {
@@ -128,155 +357,174 @@ func (self *hostedServiceRegistry) queue(event terminatorEvent) {
 	}
 }
 
-func (self *hostedServiceRegistry) scheduleRetry(terminator *edgeTerminator, queueFailed bool) {
-	terminator.establishActive.Store(false)
-	if terminator.state.CompareAndSwap(TerminatorStateEstablished, TerminatorStatePendingEstablishment) {
-		self.queue(&calculateRetry{
-			terminator:  terminator,
-			queueFailed: queueFailed,
-		})
+func (self *hostedServiceRegistry) queueWithTimeout(event terminatorEvent, timeout time.Duration) error {
+	select {
+	case self.events <- event:
+		return nil
+	case <-time.After(timeout):
+		return errors.New("timed out")
+	case <-self.env.GetCloseNotify():
+		return errors.New("closed")
 	}
 }
 
-func (self *hostedServiceRegistry) scanForRetries() {
-	self.services.Range(func(key, value any) bool {
-		terminator := value.(*edgeTerminator)
-		if terminator.state.Load() == TerminatorStatePendingEstablishment {
-			self.evaluateTerminator(terminator)
-		}
-		return true
-	})
+func (self *hostedServiceRegistry) queueEstablishTerminatorSync(terminator *edgeTerminator) {
+	if terminator.IsEstablishing() {
+		terminator.operationActive.Store(false)
+		self.establishSet[terminator.terminatorId] = terminator
+	}
 }
 
-func (self *hostedServiceRegistry) Put(hostId string, conn *edgeTerminator) {
-	self.services.Store(hostId, conn)
+func (self *hostedServiceRegistry) queueEstablishTerminatorAsync(terminator *edgeTerminator) {
+	self.queue(&queueEstablishTerminator{
+		terminator: terminator,
+	})
 }
 
-func (self *hostedServiceRegistry) Get(hostId string) (*edgeTerminator, bool) {
-	val, ok := self.services.Load(hostId)
-	if !ok {
-		return nil, false
-	}
-	ch, ok := val.(*edgeTerminator)
-	return ch, ok
+func (self *hostedServiceRegistry) queueRemoveTerminatorAsync(terminator *edgeTerminator, reason string) {
+	self.queue(&queueRemoveTerminator{
+		terminator: terminator,
+		reason:     reason,
+	})
 }
 
-func (self *hostedServiceRegistry) GetTerminatorForListener(listenerId string) *edgeTerminator {
-	var result *edgeTerminator
-	self.services.Range(func(key, value interface{}) bool {
-		terminator := value.(*edgeTerminator)
-		if terminator.listenerId == listenerId {
-			result = terminator
-			return false
-		}
-		return true
+func (self *hostedServiceRegistry) requeueRemoveTerminatorAsync(terminator *edgeTerminator) {
+	self.queue(&requeueRemoveTerminator{
+		terminator: terminator,
 	})
-	return result
 }
 
-func (self *hostedServiceRegistry) Delete(hostId string) {
-	self.services.Delete(hostId)
+func (self *hostedServiceRegistry) requeueRemoveTerminatorSync(terminator *edgeTerminator) {
+	existing, _ := self.terminators.Get(terminator.terminatorId)
+	if existing == nil || existing == terminator && terminator.state.Load() == TerminatorStateDeleting { // make sure we're still the current terminator
+		terminator.operationActive.Store(false)
+		self.deleteSet[terminator.terminatorId] = terminator
+	}
 }
 
-func (self *hostedServiceRegistry) cleanupServices(proxy *edgeClientConn) {
-	self.services.Range(func(key, value interface{}) bool {
-		terminator := value.(*edgeTerminator)
-		if terminator.edgeClientConn == proxy {
-			terminator.close(false, true, "channel closed") // don't notify, channel is already closed, we can't send messages
-			self.services.Delete(key)
-		}
-		return true
-	})
+func (self *hostedServiceRegistry) queueRemoveTerminatorSync(terminator *edgeTerminator, reason string) {
+	existing, _ := self.terminators.Get(terminator.terminatorId)
+	if existing == nil || existing == terminator { // make sure we're still the current terminator
+		self.queueRemoveTerminatorUnchecked(terminator, reason)
+	}
 }
 
-func (self *hostedServiceRegistry) cleanupDuplicates(newest *edgeTerminator) {
-	self.services.Range(func(key, value interface{}) bool {
-		terminator := value.(*edgeTerminator)
-		if terminator != newest && newest.token == terminator.token && newest.instance == terminator.instance {
-			terminator.close(false, true, "duplicate terminator") // don't notify, channel is already closed, we can't send messages
-			self.services.Delete(key)
-			pfxlog.Logger().WithField("routerId", terminator.edgeClientConn.listener.id.Token).
-				WithField("token", terminator.token).
-				WithField("instance", terminator.instance).
-				WithField("terminatorId", terminator.terminatorId.Load()).
-				WithField("duplicateOf", newest.terminatorId.Load()).
-				Info("duplicate removed")
-		}
-		return true
-	})
+func (self *hostedServiceRegistry) queueRemoveTerminatorUnchecked(terminator *edgeTerminator, reason string) {
+	terminator.setState(TerminatorStateDeleting, reason)
+	terminator.operationActive.Store(false)
+	self.deleteSet[terminator.terminatorId] = terminator
 }
 
-func (self *hostedServiceRegistry) unbindSession(connId uint32, sessionToken string, proxy *edgeClientConn) bool {
-	atLeastOneRemoved := false
-	self.services.Range(func(key, value interface{}) bool {
-		terminator := value.(*edgeTerminator)
-		if terminator.MsgChannel.Id() == connId && terminator.token == sessionToken && terminator.edgeClientConn == proxy {
-			terminator.close(false, true, "unbind successful") // don't notify, sdk asked us to unbind
-			self.services.Delete(key)
-			pfxlog.Logger().WithField("routerId", terminator.edgeClientConn.listener.id.Token).
-				WithField("token", sessionToken).
-				WithField("terminatorId", terminator.terminatorId.Load()).
-				Info("terminator removed")
-			atLeastOneRemoved = true
-		}
-		return true
+func (self *hostedServiceRegistry) markEstablished(terminator *edgeTerminator, reason string) {
+	self.queue(&markEstablishedEvent{
+		terminator: terminator,
+		reason:     reason,
 	})
-	return atLeastOneRemoved
 }
 
-func (self *hostedServiceRegistry) getRelatedTerminators(sessionToken string, proxy *edgeClientConn) []*edgeTerminator {
-	var result []*edgeTerminator
-	self.services.Range(func(key, value interface{}) bool {
-		terminator := value.(*edgeTerminator)
-		if terminator.token == sessionToken && terminator.edgeClientConn == proxy {
-			result = append(result, terminator)
+func (self *hostedServiceRegistry) scanForRetries() {
+	var retryList []*edgeTerminator
+
+	self.terminators.IterCb(func(_ string, terminator *edgeTerminator) {
+		state := terminator.state.Load()
+		if state.IsWorkRequired() && time.Since(terminator.lastAttempt) > 2*time.Minute {
+			retryList = append(retryList, terminator)
 		}
-		return true
 	})
-	return result
+
+	for _, terminator := range retryList {
+		state := terminator.state.Load()
+		if state == TerminatorStateEstablishing {
+			self.queueEstablishTerminatorSync(terminator)
+		} else if state == TerminatorStateDeleting {
+			self.requeueRemoveTerminatorSync(terminator)
+		}
+	}
 }
 
-func (self *hostedServiceRegistry) evaluateTerminator(terminator *edgeTerminator) {
-	log := logrus.
-		WithField("terminatorId", terminator.terminatorId.Load()).
-		WithField("state", terminator.state.Load()).
-		WithField("token", terminator.token)
+func (self *hostedServiceRegistry) PutV1(token string, terminator *edgeTerminator) {
+	self.terminators.Set(token, terminator)
+}
 
-	if terminator.edgeClientConn.ch.IsClosed() {
-		log.Info("terminator sdk channel closed, not trying to establish")
-		return
-	}
+func (self *hostedServiceRegistry) Put(terminator *edgeTerminator) {
+	self.terminators.Set(terminator.terminatorId, terminator)
+}
 
-	if terminator.terminatorId.Load() == "" {
-		log.Info("terminator has been closed, not trying to establish")
-		return
+func (self *hostedServiceRegistry) Get(terminatorId string) (*edgeTerminator, bool) {
+	return self.terminators.Get(terminatorId)
+}
+
+func (self *hostedServiceRegistry) Delete(terminatorId string) {
+	self.terminators.Remove(terminatorId)
+}
+
+func (self *hostedServiceRegistry) Remove(terminator *edgeTerminator, reason string) bool {
+	removed := self.terminators.RemoveCb(terminator.terminatorId, func(key string, v *edgeTerminator, exists bool) bool {
+		return v == terminator
+	})
+	if removed {
+		pfxlog.Logger().WithField("terminatorId", terminator.terminatorId).
+			WithField("reason", reason).
+			Info("terminator removed from router set")
 	}
+	return removed
+}
 
-	tryEstablish := terminator.state.Load() == TerminatorStatePendingEstablishment && terminator.nextAttempt.Before(time.Now())
+func (self *hostedServiceRegistry) cleanupServices(ch channel.Channel) {
+	self.queue(&channelClosedEvent{
+		ch: ch,
+	})
+}
 
-	if tryEstablish && terminator.establishActive.CompareAndSwap(false, true) {
-		if !terminator.state.CompareAndSwap(TerminatorStatePendingEstablishment, TerminatorStateEstablishing) {
-			log.Infof("terminator in state %s, not pending establishment, not queueing", terminator.state.Load())
-			return
+func (self *hostedServiceRegistry) cleanupDuplicates(newest *edgeTerminator) {
+	var toClose []*edgeTerminator
+	self.terminators.IterCb(func(_ string, terminator *edgeTerminator) {
+		if terminator != newest && newest.token == terminator.token && newest.instance == terminator.instance {
+			toClose = append(toClose, terminator)
 		}
+	})
 
-		log.Info("queuing terminator to send create")
+	for _, terminator := range toClose {
+		terminator.close(self, false, true, "duplicate terminator") // don't notify, channel is already closed, we can't send messages
+		pfxlog.Logger().WithField("routerId", terminator.edgeClientConn.listener.id.Token).
+			WithField("token", terminator.token).
+			WithField("instance", terminator.instance).
+			WithField("terminatorId", terminator.terminatorId).
+			WithField("duplicateOf", newest.terminatorId).
+			Info("duplicate removed")
+	}
+}
 
-		err := self.env.GetRateLimiterPool().QueueOrError(func() {
-			defer func() {
-				self.scheduleRetry(terminator, false)
-			}()
+func (self *hostedServiceRegistry) unbindSession(connId uint32, sessionToken string, proxy *edgeClientConn) bool {
+	var toClose []*edgeTerminator
+	self.terminators.IterCb(func(_ string, terminator *edgeTerminator) {
+		if terminator.MsgChannel.Id() == connId && terminator.token == sessionToken && terminator.edgeClientConn == proxy {
+			toClose = append(toClose, terminator)
+		}
+	})
 
-			if err := self.establishTerminator(terminator); err != nil {
-				log.WithError(err).Error("error establishing terminator")
-			}
-		})
+	atLeastOneRemoved := false
+	for _, terminator := range toClose {
+		terminator.close(self, false, true, "unbind successful") // don't notify, sdk asked us to unbind
+		pfxlog.Logger().WithField("routerId", terminator.edgeClientConn.listener.id.Token).
+			WithField("token", sessionToken).
+			WithField("connId", connId).
+			WithField("terminatorId", terminator.terminatorId).
+			Info("terminator removed")
+		atLeastOneRemoved = true
+	}
+	return atLeastOneRemoved
+}
 
-		if err != nil {
-			log.Info("rate limited: unable to queue to establish")
-			self.scheduleRetry(terminator, true)
+func (self *hostedServiceRegistry) getRelatedTerminators(connId uint32, sessionToken string, proxy *edgeClientConn) []*edgeTerminator {
+	var related []*edgeTerminator
+	self.terminators.IterCb(func(_ string, terminator *edgeTerminator) {
+		if terminator.MsgChannel.Id() == connId && terminator.token == sessionToken && terminator.edgeClientConn == proxy {
+			related = append(related, terminator)
 		}
-	}
+	})
+
+	return related
 }
 
 func (self *hostedServiceRegistry) establishTerminator(terminator *edgeTerminator) error {
@@ -284,16 +532,11 @@ func (self *hostedServiceRegistry) establishTerminator(terminator *edgeTerminato
 
 	log := pfxlog.Logger().
 		WithField("routerId", factory.env.GetRouterId().Token).
-		WithField("terminatorId", terminator.terminatorId.Load()).
+		WithField("terminatorId", terminator.terminatorId).
 		WithField("token", terminator.token)
 
-	terminatorId := terminator.terminatorId.Load()
-	if terminatorId == "" {
-		return fmt.Errorf("edge link is closed, stopping terminator creation for terminator %s", terminatorId)
-	}
-
 	request := &edge_ctrl_pb.CreateTerminatorV2Request{
-		Address:        terminatorId,
+		Address:        terminator.terminatorId,
 		SessionToken:   terminator.token,
 		Fingerprints:   terminator.edgeClientConn.fingerprints.Prints(),
 		PeerData:       terminator.hostData,
@@ -303,7 +546,6 @@ func (self *hostedServiceRegistry) establishTerminator(terminator *edgeTerminato
 		InstanceSecret: terminator.instanceSecret,
 	}
 
-	timeout := factory.ctrls.DefaultRequestTimeout()
 	ctrlCh := factory.ctrls.AnyCtrlChannel()
 	if ctrlCh == nil {
 		errStr := "no controller available, cannot create terminator"
@@ -313,10 +555,19 @@ func (self *hostedServiceRegistry) establishTerminator(terminator *edgeTerminato
 
 	log.Info("sending create terminator v2 request")
 
-	return protobufs.MarshalTyped(request).WithTimeout(timeout).SendAndWaitForWire(ctrlCh)
+	queued, err := ctrlCh.TrySend(protobufs.MarshalTyped(request).ToSendable())
+	if err != nil {
+		return err
+	}
+	if !queued {
+		return errors.New("channel too busy")
+	}
+	return nil
 }
 
 func (self *hostedServiceRegistry) HandleCreateTerminatorResponse(msg *channel.Message, _ channel.Channel) {
+	defer self.triggerEvaluates()
+
 	log := pfxlog.Logger().WithField("routerId", self.env.GetRouterId().Token)
 
 	response := &edge_ctrl_pb.CreateTerminatorV2Response{}
@@ -334,60 +585,380 @@ func (self *hostedServiceRegistry) HandleCreateTerminatorResponse(msg *channel.M
 		return
 	}
 
-	log = log.WithField("lifetime", time.Since(terminator.createTime))
+	log = log.WithField("lifetime", time.Since(terminator.createTime)).
+		WithField("connId", terminator.MsgChannel.Id())
 
 	if response.Result == edge_ctrl_pb.CreateTerminatorResult_FailedBusy {
 		log.Info("controller too busy to handle create terminator, retrying later")
+		if rateLimitCallback := terminator.GetAndClearRateLimitCallback(); rateLimitCallback != nil {
+			rateLimitCallback.Backoff()
+		}
+		self.queueEstablishTerminatorAsync(terminator)
 		return
 	}
 
 	if response.Result != edge_ctrl_pb.CreateTerminatorResult_Success {
+		if rateLimitCallback := terminator.GetAndClearRateLimitCallback(); rateLimitCallback != nil {
+			rateLimitCallback.Success()
+		}
+
+		terminator.operationActive.Store(false)
 		if terminator.establishCallback != nil {
-			terminator.establishCallback(false, response.Msg)
+			terminator.establishCallback(response.Result)
 		}
-		terminator.close(true, false, response.Msg)
+		terminator.close(self, true, false, response.Msg)
 		return
 	}
 
-	if terminator.state.CompareAndSwap(TerminatorStateEstablishing, TerminatorStateEstablished) {
-		log.Info("received terminator created notification")
-	} else {
-		log.Info("received additional terminator created notification")
-	}
+	self.markEstablished(terminator, "create notification received")
 
-	isValid := true
-	if terminator.postValidate {
-		if result, err := terminator.inspect(true); err != nil {
-			log.WithError(err).Error("error validating terminator after create")
-		} else if result.Type != edge.ConnTypeBind {
-			log.WithError(err).Error("terminator invalid in sdk after create, closed")
-			isValid = false
-		} else {
-			log.Info("terminator validated successfully")
-		}
-	}
+	// notify the sdk that the terminator was established
+	terminator.establishCallback(response.Result)
 
-	if isValid && terminator.establishCallback != nil {
-		terminator.establishCallback(true, "terminator established")
-	}
+	// we don't need to call inspect here, as the controller will be doing a post-create check
+	// to verify that everything is still in place
 }
 
 func (self *hostedServiceRegistry) HandleReconnect() {
 	var restablishList []*edgeTerminator
-	self.services.Range(func(key, value interface{}) bool {
-		terminator := value.(*edgeTerminator)
-		if terminator.state.CompareAndSwap(TerminatorStateEstablished, TerminatorStatePendingEstablishment) {
+	self.terminators.IterCb(func(_ string, terminator *edgeTerminator) {
+		if terminator.updateState(TerminatorStateEstablished, TerminatorStateEstablishing, "reconnecting") {
 			restablishList = append(restablishList, terminator)
 		}
-		return true
 	})
 
 	// wait for verify terminator events to come in
 	time.Sleep(10 * time.Second)
 
 	for _, terminator := range restablishList {
-		if terminator.state.Load() == TerminatorStatePendingEstablishment {
-			self.EstablishTerminator(terminator)
+		if terminator.state.Load() == TerminatorStateEstablishing {
+			self.queueEstablishTerminatorAsync(terminator)
 		}
 	}
 }
+
+func (self *hostedServiceRegistry) Inspect(timeout time.Duration) *inspect.SdkTerminatorInspectResult {
+	evt := &inspectTerminatorsEvent{
+		result: atomic.Pointer[[]*inspect.SdkTerminatorInspectDetail]{},
+		done:   make(chan struct{}),
+	}
+
+	// if we can't queue, grab the results in a non-thread-safe fashion
+	if err := self.queueWithTimeout(evt, timeout); err != nil {
+		evt.handle(self)
+	}
+
+	result := &inspect.SdkTerminatorInspectResult{}
+
+	var err error
+	result.Entries, err = evt.GetResults(timeout)
+	if err != nil {
+		result.Errors = append(result.Errors, err.Error())
+	}
+	return result
+}
+
+func (self *hostedServiceRegistry) checkForExistingListenerId(terminator *edgeTerminator) (listenerIdCheckResult, error) {
+	defaultResult := listenerIdCheckResult{
+		replaceExisting: false,
+		terminator:      terminator,
+	}
+
+	if terminator.listenerId == "" {
+		self.Put(terminator)
+		return defaultResult, nil
+	}
+
+	event := &findMatchingEvent{
+		terminator: terminator,
+		resultC:    make(chan listenerIdCheckResult, 1),
+	}
+	self.queue(event)
+
+	select {
+	case result := <-event.resultC:
+		return result, nil
+	case <-self.env.GetCloseNotify():
+		return defaultResult, errors.New("registry stopped")
+	case <-time.After(100 * time.Millisecond):
+
+		// if processing has already started, we need to wait for it to finish
+		// otherwise, we can return here
+		if event.cancelGate.CompareAndSwap(false, true) {
+			pfxlog.Logger().WithField("terminatorId", terminator.terminatorId).
+				WithField("listenerId", terminator.listenerId).
+				Info("unable to check for existing terminators with matching listenerId")
+
+			self.Put(terminator)
+			return defaultResult, nil
+		}
+
+		select {
+		case result := <-event.resultC:
+			return result, nil
+		case <-self.env.GetCloseNotify():
+			return defaultResult, errors.New("registry stopped")
+		}
+	}
+}
+
+func (self *hostedServiceRegistry) handleSdkReturnedInvalid(terminator *edgeTerminator, notifyCtrl bool) invalidTerminatorRemoveResult {
+	event := &sdkTerminatorInvalidEvent{
+		terminator: terminator,
+		resultC:    make(chan invalidTerminatorRemoveResult, 1),
+		notifyCtrl: notifyCtrl,
+	}
+	self.queue(event)
+
+	select {
+	case result := <-event.resultC:
+		return result
+	case <-time.After(100 * time.Millisecond):
+		if !self.terminators.Has(terminator.terminatorId) {
+			return invalidTerminatorRemoveResult{
+				existed: false,
+				removed: false,
+			}
+		}
+
+		return invalidTerminatorRemoveResult{
+			err: errors.New("operation timed out"),
+		}
+	}
+}
+
+type inspectTerminatorsEvent struct {
+	result atomic.Pointer[[]*inspect.SdkTerminatorInspectDetail]
+	done   chan struct{}
+}
+
+func (self *inspectTerminatorsEvent) handle(registry *hostedServiceRegistry) {
+	var result []*inspect.SdkTerminatorInspectDetail
+	registry.terminators.IterCb(func(key string, terminator *edgeTerminator) {
+		detail := &inspect.SdkTerminatorInspectDetail{
+			Key:             key,
+			Id:              terminator.terminatorId,
+			State:           terminator.state.Load().String(),
+			Token:           terminator.token,
+			ListenerId:      terminator.listenerId,
+			Instance:        terminator.instance,
+			Cost:            terminator.cost,
+			Precedence:      terminator.precedence.String(),
+			AssignIds:       terminator.assignIds,
+			V2:              terminator.v2,
+			SupportsInspect: terminator.supportsInspect,
+			OperationActive: terminator.operationActive.Load(),
+			CreateTime:      terminator.createTime.Format("2006-01-02 15:04:05"),
+			LastAttempt:     terminator.lastAttempt.Format("2006-01-02 15:04:05"),
+		}
+		result = append(result, detail)
+	})
+
+	self.result.Store(&result)
+	close(self.done)
+}
+
+func (self *inspectTerminatorsEvent) GetResults(timeout time.Duration) ([]*inspect.SdkTerminatorInspectDetail, error) {
+	select {
+	case <-self.done:
+		return *self.result.Load(), nil
+	case <-time.After(timeout):
+		return nil, errors.New("timed out waiting for result")
+	}
+}
+
+type channelClosedEvent struct {
+	ch channel.Channel
+}
+
+func (self *channelClosedEvent) handle(registry *hostedServiceRegistry) {
+	registry.terminators.IterCb(func(_ string, terminator *edgeTerminator) {
+		if terminator.MsgChannel.Channel == self.ch {
+			if terminator.v2 {
+				// we're iterating the map right now, so the terminator can't have changed
+				registry.queueRemoveTerminatorUnchecked(terminator, "channel closed")
+			} else {
+				// don't notify, channel is already closed, we can't send messages
+				go terminator.close(registry, false, true, "channel closed")
+			}
+		}
+	})
+}
+
+type listenerIdCheckResult struct {
+	replaceExisting bool
+	terminator      *edgeTerminator
+	previous        *edgeTerminator
+}
+
+type findMatchingEvent struct {
+	terminator *edgeTerminator
+	resultC    chan listenerIdCheckResult
+	cancelGate atomic.Bool
+}
+
+func (self *findMatchingEvent) handle(registry *hostedServiceRegistry) {
+	if !self.cancelGate.CompareAndSwap(false, true) {
+		return
+	}
+
+	// NOTE: We need to store the terminator in the map before we exit. If we do it later,
+	// another process for the same listener id might be in this code, and then we've got
+	// a race condition for which will end up in the map
+
+	var existingList []*edgeTerminator
+	registry.terminators.IterCb(func(_ string, terminator *edgeTerminator) {
+		if terminator.v2 && terminator.listenerId == self.terminator.listenerId {
+			existingList = append(existingList, terminator)
+		}
+	})
+
+	if len(existingList) == 0 {
+		registry.Put(self.terminator)
+		self.resultC <- listenerIdCheckResult{
+			terminator:      self.terminator,
+			replaceExisting: false,
+		}
+		return
+	}
+
+	log := pfxlog.ContextLogger(self.terminator.edgeClientConn.ch.Label()).
+		WithField("token", self.terminator.token).
+		WithField("routerId", self.terminator.edgeClientConn.listener.id.Token).
+		WithField("listenerId", self.terminator.listenerId).
+		WithField("connId", self.terminator.MsgChannel.Id()).
+		WithField("terminatorId", self.terminator.terminatorId)
+
+	for _, existing := range existingList {
+		matches := self.terminator.edgeClientConn == existing.edgeClientConn &&
+			self.terminator.token == existing.token &&
+			self.terminator.MsgChannel.Id() == existing.MsgChannel.Id() &&
+			existing.state.Load() != TerminatorStateDeleting
+
+		if matches {
+			log = log.WithField("existingTerminatorId", existing.terminatorId)
+			log.Info("duplicate bind request")
+			self.resultC <- listenerIdCheckResult{
+				terminator:      self.terminator,
+				replaceExisting: true,
+				previous:        existing,
+			}
+			return
+		}
+	}
+
+	for _, existing := range existingList {
+		if !existing.IsDeleting() {
+			self.terminator.replace(existing)
+			registry.Put(self.terminator)
+
+			// sometimes things happen close together. we need to try to notify replaced terminators
+			// that they're being closed in case they're the newer, still open connection
+			existing.close(registry, true, false, "found a newer terminator for listener id")
+			existing.setState(TerminatorStateDeleting, "newer terminator found for listener id")
+
+			log = log.WithField("existingTerminatorId", existing.terminatorId)
+			log.Info("taking over terminator from existing bind")
+
+			self.resultC <- listenerIdCheckResult{
+				terminator:      self.terminator,
+				replaceExisting: true,
+				previous:        existing,
+			}
+			return
+		}
+	}
+
+	// can't reuse terminator ID, if the existing terminator is being deleted
+	log.Info("existing terminator being deleted, need to establish new terminator")
+	registry.Put(self.terminator)
+	self.resultC <- listenerIdCheckResult{
+		terminator:      self.terminator,
+		replaceExisting: false,
+		previous:        existingList[0],
+	}
+}
+
+type invalidTerminatorRemoveResult struct {
+	err     error
+	existed bool
+	removed bool
+}
+
+type sdkTerminatorInvalidEvent struct {
+	terminator *edgeTerminator
+	resultC    chan invalidTerminatorRemoveResult
+	notifyCtrl bool
+}
+
+func (self *sdkTerminatorInvalidEvent) handle(registry *hostedServiceRegistry) {
+	self.resultC <- self.removeTerminator(registry)
+}
+
+func (self *sdkTerminatorInvalidEvent) removeTerminator(registry *hostedServiceRegistry) invalidTerminatorRemoveResult {
+	existing, _ := registry.terminators.Get(self.terminator.terminatorId)
+
+	if existing == nil {
+		return invalidTerminatorRemoveResult{
+			existed: false,
+			removed: false,
+		}
+	}
+
+	if existing != self.terminator {
+		return invalidTerminatorRemoveResult{
+			existed: true,
+			removed: false,
+		}
+	}
+
+	if self.notifyCtrl {
+		registry.queueRemoveTerminatorSync(self.terminator, "query to sdk indicated terminator is invalid")
+		return invalidTerminatorRemoveResult{
+			existed: true,
+			removed: true,
+		}
+
+	}
+
+	existed := false
+	removed := registry.terminators.RemoveCb(self.terminator.terminatorId, func(key string, v *edgeTerminator, exists bool) bool {
+		existed = exists
+		return v == self.terminator
+	})
+	if removed {
+		pfxlog.Logger().WithField("terminatorId", self.terminator.terminatorId).
+			WithField("reason", "query to sdk indicated terminator is invalid").
+			Info("terminator removed from router set")
+	}
+
+	return invalidTerminatorRemoveResult{
+		existed: existed,
+		removed: removed,
+	}
+}
+
+type markEstablishedEvent struct {
+	terminator *edgeTerminator
+	reason     string
+}
+
+func (self *markEstablishedEvent) handle(registry *hostedServiceRegistry) {
+	log := pfxlog.Logger().
+		WithField("routerId", registry.env.GetRouterId().Token).
+		WithField("terminatorId", self.terminator.terminatorId).
+		WithField("lifetime", time.Since(self.terminator.createTime)).
+		WithField("connId", self.terminator.MsgChannel.Id())
+
+	if rateLimitCallback := self.terminator.GetAndClearRateLimitCallback(); rateLimitCallback != nil {
+		rateLimitCallback.Success()
+	}
+
+	if !self.terminator.updateState(TerminatorStateEstablishing, TerminatorStateEstablished, self.reason) {
+		log.Info("received additional terminator created notification")
+	} else {
+		log.Info("terminator established")
+	}
+
+	self.terminator.operationActive.Store(false)
+}
diff --git a/router/xgress_edge/listener.go b/router/xgress_edge/listener.go
index 630e7d803..b9a678423 100644
--- a/router/xgress_edge/listener.go
+++ b/router/xgress_edge/listener.go
@@ -20,13 +20,13 @@ import (
 	"encoding/binary"
 	"fmt"
 	"github.com/openziti/ziti/common/ctrl_msg"
+	"github.com/openziti/ziti/controller/idgen"
 	"time"
 
 	"github.com/openziti/ziti/common/capabilities"
 	"github.com/openziti/ziti/common/cert"
 	fabricMetrics "github.com/openziti/ziti/common/metrics"
 	"github.com/openziti/ziti/common/pb/edge_ctrl_pb"
-	"github.com/openziti/ziti/controller/idgen"
 	"github.com/pkg/errors"
 	"google.golang.org/protobuf/proto"
 
@@ -109,10 +109,10 @@ type edgeClientConn struct {
 	idSeq        uint32
 }
 
-func (self *edgeClientConn) HandleClose(_ channel.Channel) {
+func (self *edgeClientConn) HandleClose(ch channel.Channel) {
 	log := pfxlog.ContextLogger(self.ch.Label())
 	log.Debugf("closing")
-	self.listener.factory.hostedServices.cleanupServices(self)
+	self.listener.factory.hostedServices.cleanupServices(ch)
 	self.msgMux.Close()
 }
 
@@ -305,7 +305,7 @@ func (self *edgeClientConn) processBindV1(req *channel.Message, ch channel.Chann
 
 	log.Debug("establishing listener")
 
-	messageSink := &edgeTerminator{
+	terminator := &edgeTerminator{
 		MsgChannel:     *edge.NewEdgeMsgChannel(self.ch, connId),
 		edgeClientConn: self,
 		token:          token,
@@ -314,11 +314,11 @@ func (self *edgeClientConn) processBindV1(req *channel.Message, ch channel.Chann
 	}
 
 	// need to remove session remove listener on close
-	messageSink.onClose = self.listener.factory.stateManager.AddEdgeSessionRemovedListener(token, func(token string) {
-		messageSink.close(true, true, "session ended")
+	terminator.onClose = self.listener.factory.stateManager.AddEdgeSessionRemovedListener(token, func(token string) {
+		terminator.close(self.listener.factory.hostedServices, true, true, "session ended")
 	})
 
-	self.listener.factory.hostedServices.Put(token, messageSink)
+	self.listener.factory.hostedServices.PutV1(token, terminator)
 
 	terminatorIdentity, _ := req.GetStringHeader(edge.TerminatorIdentityHeader)
 	var terminatorIdentitySecret []byte
@@ -340,18 +340,21 @@ func (self *edgeClientConn) processBindV1(req *channel.Message, ch channel.Chann
 	responseMsg, err := protobufs.MarshalTyped(request).WithTimeout(timeout).SendForReply(ctrlCh)
 	if err = xgress_common.CheckForFailureResult(responseMsg, err, edge_ctrl_pb.ContentType_CreateTerminatorResponseType); err != nil {
 		log.WithError(err).Warn("error creating terminator")
-		messageSink.close(false, false, "") // don't notify here, as we're notifying next line with a response
+		terminator.close(self.listener.factory.hostedServices, false, false, "") // don't notify here, as we're notifying next line with a response
 		self.sendStateClosedReply(err.Error(), req)
 		return
 	}
 
 	terminatorId := string(responseMsg.Body)
-	messageSink.terminatorId.Store(terminatorId)
-	log = log.WithField("terminatorId", terminatorIdentity)
+	terminator.lock.Lock()
+	terminator.terminatorId = terminatorId
+	terminator.lock.Unlock()
 
-	if messageSink.MsgChannel.IsClosed() {
+	log = log.WithField("terminatorId", terminatorId)
+
+	if terminator.MsgChannel.IsClosed() {
 		log.Warn("edge channel closed while setting up terminator. cleaning up terminator now")
-		messageSink.close(false, true, "edge channel closed")
+		terminator.close(self.listener.factory.hostedServices, false, true, "edge channel closed")
 		return
 	}
 
@@ -370,43 +373,26 @@ func (self *edgeClientConn) processBindV2(req *channel.Message, ch channel.Chann
 		WithFields(edge.GetLoggerFields(req)).
 		WithField("routerId", self.listener.id.Token)
 
+	if self.listener.factory.stateManager.WasSessionRecentlyRemoved(token) {
+		log.Info("invalid session, not establishing terminator")
+		self.sendStateClosedReply("invalid session", req)
+		return
+	}
+
 	connId, found := req.GetUint32Header(edge.ConnIdHeader)
 	if !found {
 		pfxlog.Logger().Errorf("connId not set. unable to process bind message")
 		return
 	}
 
-	var terminatorId string
+	terminatorId := idgen.NewUUIDString()
+	log = log.WithField("bindConnId", connId).WithField("terminatorId", terminatorId)
 
 	listenerId, _ := req.GetStringHeader(edge.ListenerId)
 	if listenerId != "" {
 		log = log.WithField("listenerId", listenerId)
-		if terminator := self.listener.factory.hostedServices.GetTerminatorForListener(listenerId); terminator != nil {
-			terminatorId = terminator.terminatorId.Load()
-			log = log.WithField("terminatorId", terminatorId)
-
-			// everything is the same, we can reuse the terminator
-			if terminator.edgeClientConn == self && terminator.token == token {
-				log.Info("duplicate create terminator request")
-				self.sendStateConnectedReply(req, nil)
-				return
-			}
-
-			if terminator.terminatorId.CompareAndSwap(terminatorId, "") {
-				log.Info("replacing existing terminator")
-				self.listener.factory.hostedServices.Delete(terminatorId)
-			} else {
-				terminatorId = idgen.NewUUIDString()
-				log.Infof("unable to replace existing terminator, as it's being shut down, creating new one with id %s", terminatorId)
-			}
-		}
 	}
 
-	if terminatorId == "" {
-		terminatorId = idgen.NewUUIDString()
-	}
-
-	log = log.WithField("bindConnId", connId).WithField("terminatorId", terminatorId)
 	terminatorInstance, _ := req.GetStringHeader(edge.TerminatorIdentityHeader)
 
 	assignIds, _ := req.GetBoolHeader(edge.RouterProvidedConnId)
@@ -437,63 +423,83 @@ func (self *edgeClientConn) processBindV2(req *channel.Message, ch channel.Chann
 		hostData[edge.PublicKeyHeader] = pubKey
 	}
 
-	postValidate, _ := req.GetBoolHeader(edge.SupportsInspectHeader)
+	supportsInspect, _ := req.GetBoolHeader(edge.SupportsInspectHeader)
 	notifyEstablished, _ := req.GetBoolHeader(edge.SupportsBindSuccessHeader)
 
 	terminator := &edgeTerminator{
-		MsgChannel:     *edge.NewEdgeMsgChannel(self.ch, connId),
-		edgeClientConn: self,
-		token:          token,
-		cost:           cost,
-		precedence:     precedence,
-		instance:       terminatorInstance,
-		instanceSecret: terminatorInstanceSecret,
-		hostData:       hostData,
-		assignIds:      assignIds,
-		v2:             true,
-		postValidate:   postValidate,
-		createTime:     time.Now(),
+		terminatorId:    terminatorId,
+		MsgChannel:      *edge.NewEdgeMsgChannel(self.ch, connId),
+		edgeClientConn:  self,
+		token:           token,
+		listenerId:      listenerId,
+		cost:            cost,
+		precedence:      precedence,
+		instance:        terminatorInstance,
+		instanceSecret:  terminatorInstanceSecret,
+		hostData:        hostData,
+		assignIds:       assignIds,
+		v2:              true,
+		supportsInspect: supportsInspect,
+		createTime:      time.Now(),
+	}
+
+	terminator.state.Store(TerminatorStateEstablishing)
+
+	checkResult, err := self.listener.factory.hostedServices.checkForExistingListenerId(terminator)
+	if err != nil {
+		log.WithError(err).Error("error, cancelling processing")
+		return
 	}
 
-	terminator.establishCallback = func(ok bool, msg string) {
-		if ok {
-			self.sendStateConnectedReply(req, nil)
-
-			if notifyEstablished {
-				notifyMsg := channel.NewMessage(edge.ContentTypeBindSuccess, nil)
-				notifyMsg.PutUint32Header(edge.ConnIdHeader, terminator.MsgChannel.Id())
+	terminator = checkResult.terminator
+	if terminator.state.Load() == TerminatorStateDeleting {
+		return
+	}
 
-				if err := notifyMsg.WithTimeout(time.Second * 30).Send(terminator.MsgChannel.Channel); err != nil {
-					log.WithError(err).Error("failed to send bind success")
-				}
-			}
-		} else {
-			self.sendStateClosedReply(msg, req)
-		}
+	if checkResult.previous == nil || checkResult.previous.token != token {
+		// need to remove session remove listener on close
+		terminator.onClose = self.listener.factory.stateManager.AddEdgeSessionRemovedListener(token, func(token string) {
+			terminator.close(self.listener.factory.hostedServices, true, true, "session ended")
+		})
 	}
 
-	terminator.terminatorId.Store(terminatorId)
-	terminator.state.Store(TerminatorStatePendingEstablishment)
+	terminator.establishCallback = func(result edge_ctrl_pb.CreateTerminatorResult) {
+		if result == edge_ctrl_pb.CreateTerminatorResult_Success && notifyEstablished {
+			notifyMsg := channel.NewMessage(edge.ContentTypeBindSuccess, nil)
+			notifyMsg.PutUint32Header(edge.ConnIdHeader, terminator.MsgChannel.Id())
 
-	if self.listener.factory.stateManager.WasSessionRecentlyRemoved(token) {
-		log.Info("invalid session, not establishing terminator")
-		terminator.establishCallback(false, "invalid session")
-		return
+			if err := notifyMsg.WithTimeout(time.Second * 30).Send(terminator.MsgChannel.Channel); err != nil {
+				log.WithError(err).Error("failed to send bind success")
+			} else {
+				log.Info("sdk notified of terminator creation")
+			}
+		} else if result == edge_ctrl_pb.CreateTerminatorResult_FailedInvalidSession {
+			// TODO: notify of invalid session. Currently handling this using the recently removed sessions
+			//       LRU cache in state manager
+			log.Trace("invalid session")
+		}
 	}
 
-	// need to remove session remove listener on close
-	terminator.onClose = self.listener.factory.stateManager.AddEdgeSessionRemovedListener(token, func(token string) {
-		terminator.close(true, true, "session ended")
-	})
-
-	log.Info("establishing terminator")
+	self.sendStateConnectedReply(req, nil)
 
-	self.listener.factory.hostedServices.EstablishTerminator(terminator)
-	if listenerId == "" {
-		// only removed dupes with a scan if we don't have an sdk provided key
-		self.listener.factory.hostedServices.cleanupDuplicates(terminator)
+	if checkResult.replaceExisting {
+		log.Info("sending replacement terminator success to sdk")
+		terminator.establishCallback(edge_ctrl_pb.CreateTerminatorResult_Success)
+		if terminator.supportsInspect {
+			go func() {
+				if _, err := terminator.inspect(self.listener.factory.hostedServices, true, true); err != nil {
+					log.WithError(err).Info("failed to check sdk side of terminator after replace")
+				}
+			}()
+		}
+	} else {
+		log.Info("establishing terminator")
+		self.listener.factory.hostedServices.EstablishTerminator(terminator)
+		if listenerId == "" {
+			// only removed dupes with a scan if we don't have an sdk provided key
+			self.listener.factory.hostedServices.cleanupDuplicates(terminator)
+		}
 	}
-
 }
 
 func (self *edgeClientConn) processUnbind(req *channel.Message, _ channel.Channel) {
@@ -524,8 +530,10 @@ func (self *edgeClientConn) removeTerminator(ctrlCh channel.Channel, token, term
 func (self *edgeClientConn) processUpdateBind(req *channel.Message, ch channel.Channel) {
 	token := string(req.Body)
 
+	connId, _ := req.GetUint32Header(edge.ConnIdHeader)
+
 	log := pfxlog.ContextLogger(ch.Label()).WithField("token", token).WithFields(edge.GetLoggerFields(req))
-	terminators := self.listener.factory.hostedServices.getRelatedTerminators(token, self)
+	terminators := self.listener.factory.hostedServices.getRelatedTerminators(connId, token, self)
 
 	if len(terminators) == 0 {
 		log.Error("failed to update bind, no listener found")
@@ -542,7 +550,7 @@ func (self *edgeClientConn) processUpdateBind(req *channel.Message, ch channel.C
 		request := &edge_ctrl_pb.UpdateTerminatorRequest{
 			SessionToken: token,
 			Fingerprints: self.fingerprints.Prints(),
-			TerminatorId: terminator.terminatorId.Load(),
+			TerminatorId: terminator.terminatorId,
 		}
 
 		if costVal, hasCost := req.GetUint16Header(edge.CostHeader); hasCost {
@@ -561,7 +569,7 @@ func (self *edgeClientConn) processUpdateBind(req *channel.Message, ch channel.C
 			}
 		}
 
-		log = log.WithField("terminator", terminator.terminatorId.Load()).
+		log = log.WithField("terminator", terminator.terminatorId).
 			WithField("precedence", request.Precedence).
 			WithField("cost", request.Cost).
 			WithField("updatingPrecedence", request.UpdatePrecedence).
@@ -601,11 +609,11 @@ func (self *edgeClientConn) processHealthEvent(req *channel.Message, ch channel.
 	request := &edge_ctrl_pb.HealthEventRequest{
 		SessionToken: token,
 		Fingerprints: self.fingerprints.Prints(),
-		TerminatorId: terminator.terminatorId.Load(),
+		TerminatorId: terminator.terminatorId,
 		CheckPassed:  checkPassed,
 	}
 
-	log = log.WithField("terminator", terminator.terminatorId.Load()).WithField("checkPassed", checkPassed)
+	log = log.WithField("terminator", terminator.terminatorId).WithField("checkPassed", checkPassed)
 	log.Debug("sending health event")
 
 	if err := protobufs.MarshalTyped(request).Send(ctrlCh); err != nil {
diff --git a/router/xgress_edge/terminator_heap.go b/router/xgress_edge/terminator_heap.go
deleted file mode 100644
index da0e72b00..000000000
--- a/router/xgress_edge/terminator_heap.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-	(c) Copyright NetFoundry Inc.
-
-	Licensed under the Apache License, Version 2.0 (the "License");
-	you may not use this file except in compliance with the License.
-	You may obtain a copy of the License at
-
-	https://www.apache.org/licenses/LICENSE-2.0
-
-	Unless required by applicable law or agreed to in writing, software
-	distributed under the License is distributed on an "AS IS" BASIS,
-	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-	See the License for the specific language governing permissions and
-	limitations under the License.
-*/
-
-package xgress_edge
-
-type terminatorHeap []*edgeTerminator
-
-func (self terminatorHeap) Len() int {
-	return len(self)
-}
-
-func (self terminatorHeap) Less(i, j int) bool {
-	return self[i].nextAttempt.Before(self[j].nextAttempt)
-}
-
-func (self terminatorHeap) Swap(i, j int) {
-	tmp := self[i]
-	self[i] = self[j]
-	self[j] = tmp
-}
-
-func (self *terminatorHeap) Push(x any) {
-	*self = append(*self, x.(*edgeTerminator))
-}
-
-func (self *terminatorHeap) Pop() any {
-	old := *self
-	n := len(old)
-	pm := old[n-1]
-	*self = old[0 : n-1]
-	return pm
-}
diff --git a/version b/version
index 00d0c14da..94b357edf 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-0.32
+0.33
diff --git a/ziti/cmd/cmd.go b/ziti/cmd/cmd.go
index 733d373c7..64fd9fa1e 100644
--- a/ziti/cmd/cmd.go
+++ b/ziti/cmd/cmd.go
@@ -131,7 +131,7 @@ func NewCmdRoot(in io.Reader, out, err io.Writer, cmd *cobra.Command) *cobra.Com
 	agentCommands := agentcli.NewAgentCmd(p)
 	pkiCommands := pki.NewCmdPKI(out, err)
 	fabricCommand := fabric.NewFabricCmd(p)
-	edgeCommand := edge.NewCmdEdge(out, err)
+	edgeCommand := edge.NewCmdEdge(out, err, p)
 	demoCmd := demo.NewDemoCmd(p)
 
 	opsCommands := &cobra.Command{
diff --git a/ziti/cmd/database/add_debug_admin.go b/ziti/cmd/database/add_debug_admin.go
index c8b40b30b..020946241 100644
--- a/ziti/cmd/database/add_debug_admin.go
+++ b/ziti/cmd/database/add_debug_admin.go
@@ -69,7 +69,7 @@ func (action *addDebugAdminAction) run(dbFile, username, password string) {
 	boltDb, err := db.Open(dbFile)
 	action.noError(err)
 
-	fabricStores, err := db.InitStores(boltDb)
+	fabricStores, err := db.InitStores(boltDb, command.NoOpRateLimiter{})
 	action.noError(err)
 
 	dispatcher := &command.LocalDispatcher{
@@ -83,7 +83,7 @@ func (action *addDebugAdminAction) run(dbFile, username, password string) {
 		managers: controllers,
 	}
 
-	stores, err := db.InitStores(boltDb)
+	stores, err := db.InitStores(boltDb, command.NoOpRateLimiter{})
 	action.noError(err)
 
 	id := "debug-admin"
diff --git a/ziti/cmd/edge/root.go b/ziti/cmd/edge/root.go
index 3538ffbda..a8e16155a 100644
--- a/ziti/cmd/edge/root.go
+++ b/ziti/cmd/edge/root.go
@@ -19,6 +19,7 @@ package edge
 import (
 	"context"
 	"github.com/openziti/ziti/ziti/cmd/common"
+	cmdhelper "github.com/openziti/ziti/ziti/cmd/helpers"
 	"github.com/openziti/ziti/ziti/util"
 	"io"
 
@@ -29,13 +30,9 @@ import (
 var ExtraEdgeCommands []func(p common.OptionsProvider) *cobra.Command
 
 // NewCmdEdge creates a command object for the "controller" command
-func NewCmdEdge(out io.Writer, errOut io.Writer) *cobra.Command {
+func NewCmdEdge(out io.Writer, errOut io.Writer, p common.OptionsProvider) *cobra.Command {
 	cmd := util.NewEmptyParentCmd("edge", "Manage the Edge components of a Ziti network using the Ziti Edge REST API")
-	populateEdgeCommands(out, errOut, cmd)
-	return cmd
-}
 
-func populateEdgeCommands(out io.Writer, errOut io.Writer, cmd *cobra.Command) *cobra.Command {
 	cmd.AddCommand(newCreateCmd(out, errOut))
 	cmd.AddCommand(newDeleteCmd(out, errOut))
 	cmd.AddCommand(NewLoginCmd(out, errOut))
@@ -52,12 +49,25 @@ func populateEdgeCommands(out io.Writer, errOut io.Writer, cmd *cobra.Command) *
 	cmd.AddCommand(newShowCmd(out, errOut))
 	cmd.AddCommand(newReEnrollCmd(out, errOut))
 	cmd.AddCommand(NewQuickStartCmd(out, errOut, context.Background()))
-
-	p := common.NewOptionsProvider(out, errOut)
+	cmd.AddCommand(newValidateCommand(p))
 	cmd.AddCommand(enrollment.NewEnrollCommand(p))
+
 	for _, cmdF := range ExtraEdgeCommands {
 		cmd.AddCommand(cmdF(p))
 	}
 
 	return cmd
 }
+
+func newValidateCommand(p common.OptionsProvider) *cobra.Command {
+	validateCmd := &cobra.Command{
+		Use:   "validate",
+		Short: "validate model data",
+		Run: func(cmd *cobra.Command, args []string) {
+			cmdhelper.CheckErr(cmd.Help())
+		},
+	}
+
+	validateCmd.AddCommand(NewValidateServiceHostingCmd(p))
+	return validateCmd
+}
diff --git a/ziti/cmd/edge/validate_service_hosting.go b/ziti/cmd/edge/validate_service_hosting.go
new file mode 100644
index 000000000..44a62574a
--- /dev/null
+++ b/ziti/cmd/edge/validate_service_hosting.go
@@ -0,0 +1,131 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package edge
+
+import (
+	"fmt"
+	"github.com/openziti/edge-api/rest_management_api_client/service"
+	"github.com/openziti/ziti/controller/rest_client/terminator"
+	"github.com/openziti/ziti/controller/rest_model"
+	"github.com/openziti/ziti/ziti/cmd/api"
+	"github.com/openziti/ziti/ziti/cmd/common"
+	"github.com/openziti/ziti/ziti/util"
+	"github.com/spf13/cobra"
+)
+
+type validateServiceHostingAction struct {
+	api.Options
+	filter string
+}
+
+func NewValidateServiceHostingCmd(p common.OptionsProvider) *cobra.Command {
+	action := validateServiceHostingAction{
+		Options: api.Options{
+			CommonOptions: p(),
+		},
+	}
+
+	validateServiceHostingCmd := &cobra.Command{
+		Use:     "service-hosting",
+		Short:   "Validate service hosting by comparing what is allowed to host services with what actually is hosting",
+		Example: "ziti fabric validate service-hosting --filter 'name=\"test\"' --show-only-invalid",
+		Args:    cobra.ExactArgs(0),
+		RunE:    action.validateServiceHosting,
+	}
+
+	action.AddCommonFlags(validateServiceHostingCmd)
+	validateServiceHostingCmd.Flags().StringVar(&action.filter, "filter", "sort by name limit none", "Specify which services to validate")
+	return validateServiceHostingCmd
+}
+
+func (self *validateServiceHostingAction) validateServiceHosting(cmd *cobra.Command, _ []string) error {
+	client, err := util.NewEdgeManagementClient(&self.Options)
+
+	if err != nil {
+		return err
+	}
+
+	fabricClient, err := util.NewFabricManagementClient(&self.Options)
+	if err != nil {
+		return err
+	}
+
+	context, cancelContext := self.Options.TimeoutContext()
+	defer cancelContext()
+
+	result, err := client.Service.ListServices(&service.ListServicesParams{
+		Filter:  &self.filter,
+		Context: context,
+	}, nil)
+
+	if err != nil {
+		return util.WrapIfApiError(err)
+	}
+
+	policyType := "bind"
+
+	limitNoneFilter := "limit none"
+	terminatorResult, err := fabricClient.Terminator.ListTerminators(&terminator.ListTerminatorsParams{
+		Filter:  &limitNoneFilter,
+		Limit:   nil,
+		Offset:  nil,
+		Context: context,
+	})
+	if err != nil {
+		return err
+	}
+
+	terminatorsBySvcAndHost := map[string]map[string][]*rest_model.TerminatorDetail{}
+	for _, detail := range terminatorResult.Payload.Data {
+		byHost, ok := terminatorsBySvcAndHost[*detail.ServiceID]
+		if !ok {
+			byHost = map[string][]*rest_model.TerminatorDetail{}
+			terminatorsBySvcAndHost[*detail.ServiceID] = byHost
+		}
+		byHost[*detail.HostID] = append(byHost[*detail.HostID], detail)
+	}
+
+	for _, svc := range result.Payload.Data {
+		identitiesResult, err := client.Service.ListServiceIdentities(&service.ListServiceIdentitiesParams{
+			ID:         *svc.ID,
+			PolicyType: &policyType,
+			Filter:     &limitNoneFilter,
+			Context:    context,
+		}, nil)
+		if err != nil {
+			return err
+		}
+
+		identities := identitiesResult.Payload.Data
+		if len(identities) == 0 {
+			fmt.Printf("service '%s' is not hostable by any identities\n", *svc.Name)
+		}
+
+		for _, identity := range identities {
+			var list []*rest_model.TerminatorDetail
+			if byHost, ok := terminatorsBySvcAndHost[*svc.ID]; ok {
+				list = byHost[*identity.ID]
+			}
+			fmt.Printf("service %s (%s) hosted by %s (%s) with %d terminators\n",
+				*svc.Name, *svc.ID,
+				*identity.Name, *identity.ID,
+				len(list))
+		}
+	}
+
+	return nil
+}
diff --git a/ziti/cmd/fabric/inspect.go b/ziti/cmd/fabric/inspect.go
index fae970d50..4ab0dda36 100644
--- a/ziti/cmd/fabric/inspect.go
+++ b/ziti/cmd/fabric/inspect.go
@@ -5,10 +5,10 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/fatih/color"
-	"github.com/openziti/ziti/controller/rest_client/inspect"
-	"github.com/openziti/ziti/controller/rest_model"
 	"github.com/openziti/foundation/v2/errorz"
 	"github.com/openziti/foundation/v2/stringz"
+	"github.com/openziti/ziti/controller/rest_client/inspect"
+	"github.com/openziti/ziti/controller/rest_model"
 	"github.com/openziti/ziti/ziti/cmd/api"
 	"github.com/openziti/ziti/ziti/cmd/common"
 	"github.com/openziti/ziti/ziti/util"
@@ -29,7 +29,10 @@ func newInspectCmd(p common.OptionsProvider) *cobra.Command {
 	cmd.AddCommand(action.newInspectSubCmd(p, "config", "gets configuration from the requested nodes"))
 	cmd.AddCommand(action.newInspectSubCmd(p, "cluster-config", "gets a subset of cluster configuration from the requested nodes"))
 	cmd.AddCommand(action.newInspectSubCmd(p, "connected-routers", "gets information about which routers are connected to which controllers"))
+	cmd.AddCommand(action.newInspectSubCmd(p, "connected-peers", "gets information about which controllers are connected to which other controllers in the cluster"))
 	cmd.AddCommand(action.newInspectSubCmd(p, "links", "gets information from routers about their view of links"))
+	cmd.AddCommand(action.newInspectSubCmd(p, "sdk-terminators", "gets information from routers about their view of sdk terminators"))
+	cmd.AddCommand(action.newInspectSubCmd(p, "router-messaging", "gets information about pending router peer updates and terminator validations"))
 
 	inspectCircuitsAction := &InspectCircuitsAction{InspectAction: *newInspectAction(p)}
 	cmd.AddCommand(inspectCircuitsAction.newCobraCmd())
diff --git a/ziti/cmd/fabric/root.go b/ziti/cmd/fabric/root.go
index d4da670eb..8078ad537 100644
--- a/ziti/cmd/fabric/root.go
+++ b/ziti/cmd/fabric/root.go
@@ -111,6 +111,7 @@ func newValidateCommand(p common.OptionsProvider) *cobra.Command {
 
 	validateCmd.AddCommand(NewValidateTerminatorsCmd(p))
 	validateCmd.AddCommand(NewValidateRouterLinksCmd(p))
+	validateCmd.AddCommand(NewValidateRouterSdkTerminatorsCmd(p))
 	return validateCmd
 }
 
diff --git a/ziti/cmd/fabric/validate_router_sdk_terminators.go b/ziti/cmd/fabric/validate_router_sdk_terminators.go
new file mode 100644
index 000000000..2b6c489cb
--- /dev/null
+++ b/ziti/cmd/fabric/validate_router_sdk_terminators.go
@@ -0,0 +1,158 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package fabric
+
+import (
+	"fmt"
+	"github.com/michaelquigley/pfxlog"
+	"github.com/openziti/channel/v2"
+	"github.com/openziti/channel/v2/protobufs"
+	"github.com/openziti/ziti/common/pb/mgmt_pb"
+	"github.com/openziti/ziti/ziti/cmd/api"
+	"github.com/openziti/ziti/ziti/cmd/common"
+	"github.com/spf13/cobra"
+	"google.golang.org/protobuf/proto"
+	"os"
+	"time"
+)
+
+type validateRouterSdkTerminatorsAction struct {
+	api.Options
+	includeValidSdkTerminators bool
+	includeValidRouters        bool
+
+	eventNotify chan *mgmt_pb.RouterSdkTerminatorsDetails
+}
+
+func NewValidateRouterSdkTerminatorsCmd(p common.OptionsProvider) *cobra.Command {
+	action := validateRouterSdkTerminatorsAction{
+		Options: api.Options{
+			CommonOptions: p(),
+		},
+	}
+
+	validateSdkTerminatorsCmd := &cobra.Command{
+		Use:     "router-sdk-terminators <router filter>",
+		Short:   "Validate router sdk terminators",
+		Example: "ziti fabric validate router-sdk-terminators --filter 'name=\"my-router\"' --include-valid",
+		Args:    cobra.MaximumNArgs(1),
+		RunE:    action.validateRouterSdkTerminators,
+	}
+
+	action.AddCommonFlags(validateSdkTerminatorsCmd)
+	validateSdkTerminatorsCmd.Flags().BoolVar(&action.includeValidSdkTerminators, "include-valid-terminators", false, "Don't hide results for valid SdkTerminators")
+	validateSdkTerminatorsCmd.Flags().BoolVar(&action.includeValidRouters, "include-valid-routers", false, "Don't hide results for valid routers")
+	return validateSdkTerminatorsCmd
+}
+
+func (self *validateRouterSdkTerminatorsAction) validateRouterSdkTerminators(_ *cobra.Command, args []string) error {
+	closeNotify := make(chan struct{})
+	self.eventNotify = make(chan *mgmt_pb.RouterSdkTerminatorsDetails, 1)
+
+	bindHandler := func(binding channel.Binding) error {
+		binding.AddReceiveHandler(int32(mgmt_pb.ContentType_ValidateRouterSdkTerminatorsResultType), self)
+		binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) {
+			close(closeNotify)
+		}))
+		return nil
+	}
+
+	ch, err := api.NewWsMgmtChannel(channel.BindHandlerF(bindHandler))
+	if err != nil {
+		return err
+	}
+
+	filter := ""
+	if len(args) > 0 {
+		filter = args[0]
+	}
+
+	request := &mgmt_pb.ValidateRouterSdkTerminatorsRequest{
+		Filter: filter,
+	}
+
+	responseMsg, err := protobufs.MarshalTyped(request).WithTimeout(time.Duration(self.Timeout) * time.Second).SendForReply(ch)
+
+	response := &mgmt_pb.ValidateRouterSdkTerminatorsResponse{}
+	if err = protobufs.TypedResponse(response).Unmarshall(responseMsg, err); err != nil {
+		return err
+	}
+
+	if !response.Success {
+		return fmt.Errorf("failed to start sdk terminator validation: %s", response.Message)
+	}
+
+	fmt.Printf("started validation of %v routers\n", response.RouterCount)
+
+	expected := response.RouterCount
+
+	errCount := 0
+	for expected > 0 {
+		select {
+		case <-closeNotify:
+			fmt.Printf("channel closed, exiting")
+			return nil
+		case routerDetail := <-self.eventNotify:
+			result := "validation successful"
+			if !routerDetail.ValidateSuccess {
+				result = fmt.Sprintf("error: unable to validate (%s)", routerDetail.Message)
+				errCount++
+			}
+
+			routerHeaderDone := false
+			outputRouterHeader := func() {
+				fmt.Printf("routerId: %s, routerName: %v, sdk-terminators: %v, %s\n",
+					routerDetail.RouterId, routerDetail.RouterName, len(routerDetail.Details), result)
+				routerHeaderDone = true
+			}
+
+			if self.includeValidRouters || routerDetail.Message != "" {
+				outputRouterHeader()
+			}
+
+			for _, detail := range routerDetail.Details {
+				if self.includeValidSdkTerminators || !detail.IsValid {
+					if !routerHeaderDone {
+						outputRouterHeader()
+					}
+					fmt.Printf("\tid: %s, ctrlState: %v, routerState: %s, created: %s, lastAttempt: %s, reqOutstanding: %v\n",
+						detail.TerminatorId, detail.CtrlState, detail.RouterState,
+						detail.CreateTime, detail.LastAttempt, detail.OperaationActive)
+				}
+				if !detail.IsValid {
+					errCount++
+				}
+			}
+			expected--
+		}
+	}
+	fmt.Printf("%v errors found\n", errCount)
+	if errCount > 0 {
+		os.Exit(1)
+	}
+	return nil
+}
+
+func (self *validateRouterSdkTerminatorsAction) HandleReceive(msg *channel.Message, _ channel.Channel) {
+	detail := &mgmt_pb.RouterSdkTerminatorsDetails{}
+	if err := proto.Unmarshal(msg.Body, detail); err != nil {
+		pfxlog.Logger().WithError(err).Error("unable to unmarshal router sdk terminator details")
+		return
+	}
+
+	self.eventNotify <- detail
+}
diff --git a/ziti/cmd/fabric/validate_terminators.go b/ziti/cmd/fabric/validate_terminators.go
index 59c2c3d64..067d74cee 100644
--- a/ziti/cmd/fabric/validate_terminators.go
+++ b/ziti/cmd/fabric/validate_terminators.go
@@ -31,9 +31,9 @@ import (
 
 type validateTerminatorsAction struct {
 	api.Options
-	filter          string
-	fixInvalid      bool
-	showOnlyInvalid bool
+	filter       string
+	fixInvalid   bool
+	includeValid bool
 
 	eventNotify chan *mgmt_pb.TerminatorDetail
 }
@@ -55,7 +55,7 @@ func NewValidateTerminatorsCmd(p common.OptionsProvider) *cobra.Command {
 
 	action.AddCommonFlags(validateTerminatorsCmd)
 	validateTerminatorsCmd.Flags().BoolVar(&action.fixInvalid, "fix-invalid", false, "Fix invalid terminators. Usually this means deleting them.")
-	validateTerminatorsCmd.Flags().BoolVar(&action.showOnlyInvalid, "show-only-invalid", false, "Hide results for valid terminators")
+	validateTerminatorsCmd.Flags().BoolVar(&action.includeValid, "include-valid", false, "Show results for valid terminators as well")
 	validateTerminatorsCmd.Flags().StringVar(&action.filter, "filter", "", "Specify which terminators to validate")
 	return validateTerminatorsCmd
 }
@@ -103,7 +103,7 @@ func (self *validateTerminatorsAction) validateTerminators(cmd *cobra.Command, _
 			fmt.Printf("channel closed, exiting")
 			return nil
 		case detail := <-self.eventNotify:
-			if !self.showOnlyInvalid || detail.State != mgmt_pb.TerminatorState_Valid {
+			if self.includeValid || detail.State != mgmt_pb.TerminatorState_Valid {
 				fmt.Printf("id: %s, binding: %s, hostId: %s, routerId: %s, state: %s, fixed: %v, detail: %s\n",
 					detail.TerminatorId, detail.Binding, detail.HostId, detail.RouterId, detail.State.String(), detail.Fixed, detail.Detail)
 			}
diff --git a/zititest/go.mod b/zititest/go.mod
index 6245b9427..377636e53 100644
--- a/zititest/go.mod
+++ b/zititest/go.mod
@@ -14,11 +14,11 @@ require (
 	github.com/michaelquigley/pfxlog v0.6.10
 	github.com/openziti/agent v1.0.16
 	github.com/openziti/channel/v2 v2.0.119
-	github.com/openziti/edge-api v0.26.11
+	github.com/openziti/edge-api v0.26.12
 	github.com/openziti/fablab v0.5.42
 	github.com/openziti/foundation/v2 v2.0.37
 	github.com/openziti/identity v1.0.70
-	github.com/openziti/sdk-golang v0.22.28
+	github.com/openziti/sdk-golang v0.23.3
 	github.com/openziti/storage v0.2.30
 	github.com/openziti/transport/v2 v2.0.122
 	github.com/openziti/ziti v0.28.3
@@ -27,7 +27,7 @@ require (
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.8.0
 	github.com/stretchr/testify v1.8.4
-	go.etcd.io/bbolt v1.3.8
+	go.etcd.io/bbolt v1.3.9
 	golang.org/x/net v0.21.0
 	google.golang.org/protobuf v1.32.0
 	gopkg.in/yaml.v2 v2.4.0
@@ -66,7 +66,7 @@ require (
 	github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect
 	github.com/gaissmai/extnetip v0.4.0 // indirect
 	github.com/go-acme/lego/v4 v4.15.0 // indirect
-	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
+	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/go-logr/logr v1.4.1 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
@@ -76,7 +76,7 @@ require (
 	github.com/go-openapi/jsonreference v0.20.4 // indirect
 	github.com/go-openapi/loads v0.21.5 // indirect
 	github.com/go-openapi/spec v0.20.14 // indirect
-	github.com/go-openapi/strfmt v0.22.0 // indirect
+	github.com/go-openapi/strfmt v0.22.1 // indirect
 	github.com/go-openapi/swag v0.22.9 // indirect
 	github.com/go-openapi/validate v0.23.0 // indirect
 	github.com/go-resty/resty/v2 v2.11.0 // indirect
@@ -94,7 +94,7 @@ require (
 	github.com/hashicorp/golang-lru v0.6.0 // indirect
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/hashicorp/raft v1.6.0 // indirect
+	github.com/hashicorp/raft v1.6.1 // indirect
 	github.com/hashicorp/raft-boltdb v0.0.0-20220329195025-15018e9b97e0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/influxdata/influxdb-client-go/v2 v2.13.0 // indirect
@@ -179,20 +179,20 @@ require (
 	github.com/zitadel/oidc/v2 v2.12.0 // indirect
 	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
-	go.opentelemetry.io/otel v1.23.1 // indirect
-	go.opentelemetry.io/otel/metric v1.23.1 // indirect
-	go.opentelemetry.io/otel/trace v1.23.1 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
 	go4.org v0.0.0-20180809161055-417644f6feb5 // indirect
-	golang.org/x/crypto v0.19.0 // indirect
+	golang.org/x/crypto v0.21.0 // indirect
 	golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
 	golang.org/x/image v0.13.0 // indirect
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/oauth2 v0.16.0 // indirect
 	golang.org/x/sync v0.6.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
-	golang.org/x/term v0.17.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/term v0.18.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/tools v0.17.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
diff --git a/zititest/go.sum b/zititest/go.sum
index dd53cdd95..3a3f66eb1 100644
--- a/zititest/go.sum
+++ b/zititest/go.sum
@@ -209,8 +209,8 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
-github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
+github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
@@ -237,8 +237,8 @@ github.com/go-openapi/runtime v0.27.1 h1:ae53yaOoh+fx/X5Eaq8cRmavHgDma65XPZuvBqv
 github.com/go-openapi/runtime v0.27.1/go.mod h1:fijeJEiEclyS8BRurYE1DE5TLb9/KZl6eAdbzjsrlLU=
 github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
 github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
-github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI=
-github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
+github.com/go-openapi/strfmt v0.22.1 h1:5Ky8cybT4576C6Ffc+8gYji/wRXCo6Ozm8RaWjPI6jc=
+github.com/go-openapi/strfmt v0.22.1/go.mod h1:OfVoytIXJasDkkGvkb1Cceb3BPyMOwk1FgmyyEw7NYg=
 github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
 github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
 github.com/go-openapi/validate v0.23.0 h1:2l7PJLzCis4YUGEoW6eoQw3WhyM65WSIcjX6SQnlfDw=
@@ -389,8 +389,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM=
-github.com/hashicorp/raft v1.6.0 h1:tkIAORZy2GbJ2Trp5eUSggLXDPOJLXC+JJLNMMqtgtM=
-github.com/hashicorp/raft v1.6.0/go.mod h1:Xil5pDgeGwRWuX4uPUmwa+7Vagg4N804dz6mhNi6S7o=
+github.com/hashicorp/raft v1.6.1 h1:v/jm5fcYHvVkL0akByAp+IDdDSzCNCGhdO6VdB56HIM=
+github.com/hashicorp/raft v1.6.1/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0=
 github.com/hashicorp/raft-boltdb v0.0.0-20220329195025-15018e9b97e0 h1:CO8dBMLH6dvE1jTn/30ZZw3iuPsNfajshWoJTnVc5cc=
 github.com/hashicorp/raft-boltdb v0.0.0-20220329195025-15018e9b97e0/go.mod h1:nTakvJ4XYq45UXtn0DbwR4aU9ZdjlnIenpbs6Cd+FM0=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
@@ -590,8 +590,8 @@ github.com/openziti/channel/v2 v2.0.119 h1:stfSrnDqoTi78LMvQA3+NSivHjQnRrYKrgij5
 github.com/openziti/channel/v2 v2.0.119/go.mod h1:lSRJwqmbkE34DgXYEmUhVCzwcQcx65vZGE8nuBNK458=
 github.com/openziti/dilithium v0.3.3 h1:PLgQ6PMNLSTzCFbX/h98cmudgz/cU6TmjdSv5NAPD8k=
 github.com/openziti/dilithium v0.3.3/go.mod h1:vsCjI2AU/hon9e+dLhUFbCNGesJDj2ASgkySOcpmvjo=
-github.com/openziti/edge-api v0.26.11 h1:qINsfGpPBTnbuDrOq+qcMZuBdlXosqvHX7sQhLA+cM4=
-github.com/openziti/edge-api v0.26.11/go.mod h1:30SiUmR+9gOBi9HUZgXLpCO2nNCbNFVx2jwXV2Dh4Og=
+github.com/openziti/edge-api v0.26.12 h1:5VRz0cWtfQq2rhSA7Ne6amM7YNI6pQGRfNgbKt0g6kQ=
+github.com/openziti/edge-api v0.26.12/go.mod h1:tKZRUFDB9zM5J1zBS0ok2r40OhJqWykZaU9HSBQgr8w=
 github.com/openziti/fablab v0.5.42 h1:vENJKfEba2T4sSLwlKDL/IzBYfY8iHnhc4umf6IESiY=
 github.com/openziti/fablab v0.5.42/go.mod h1:HDT06y1QX8kO8ZQrgHvZmJsvc8iRybESGtlDLDII4ks=
 github.com/openziti/foundation/v2 v2.0.37 h1:7pa4vWrlwllEoLXaK2rx91AffLQJ8k5pvc92oWANavA=
@@ -604,8 +604,8 @@ github.com/openziti/metrics v1.2.45 h1:+3zqszLWyFdTgzbsQD90V0yJcC9Ek77qKaIGMQXkA
 github.com/openziti/metrics v1.2.45/go.mod h1:g6CgAEbFes2UtdfGrsR8AKkuoBVL5dkU61843uQvllM=
 github.com/openziti/runzmd v1.0.38 h1:0eFWAf7R9Thx99ue7Gql2dBavsqv4jldU8W5AIZJ8wo=
 github.com/openziti/runzmd v1.0.38/go.mod h1:WiQi+YIXxZBH1bwjr+eo/e3ftfTLEABpN0i2QIhBI9w=
-github.com/openziti/sdk-golang v0.22.28 h1:s159CT42dXug4GiJiN/kM6/ol+N2LFZ2tUk6bOpbgiI=
-github.com/openziti/sdk-golang v0.22.28/go.mod h1:BLaLvcLqAgf3JFoDPWLTj3j3X5rndo6ZejdDdkMlihQ=
+github.com/openziti/sdk-golang v0.23.3 h1:LujHMOcGoY7qeT4mtcJhzklEG2f192islXjT/jY0oQ4=
+github.com/openziti/sdk-golang v0.23.3/go.mod h1:2yXIT3b6iWVyCChwuavaJHhlHozrxoM2IB3bybbVX4M=
 github.com/openziti/secretstream v0.1.16 h1:tVanF7OpJL1MJ1gvWaRlR2i+kAbrGsxr3q6EXFOS08U=
 github.com/openziti/secretstream v0.1.16/go.mod h1:bvjGBUW/0e5MzD5S3FW3rhGASRNWAi+kTkTENZ9qRDE=
 github.com/openziti/storage v0.2.30 h1:15o8rSSgtcNCSBONt81ZRASQXJHh2L9Y0aWcHoQ81dE=
@@ -824,8 +824,8 @@ github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
 github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4=
 github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
-go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
 go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
 go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
 go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@@ -841,14 +841,14 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
-go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
-go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
-go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
 go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE=
 go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ=
-go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
-go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
@@ -876,7 +876,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -885,8 +884,9 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1099,16 +1099,18 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/zititest/models/db-sdk-hosting-test/configs/ctrl.yml.tmpl b/zititest/models/db-sdk-hosting-test/configs/ctrl.yml.tmpl
new file mode 100644
index 000000000..850bbc302
--- /dev/null
+++ b/zititest/models/db-sdk-hosting-test/configs/ctrl.yml.tmpl
@@ -0,0 +1,205 @@
+v: 3
+
+{{if .Component.GetFlag "ha"}}
+raft:
+  minClusterSize: 3
+  dataDir: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/ctrldata
+{{else}}
+db: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/ctrl.db
+{{end}}
+
+identity:
+  cert: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/certs/{{ .Component.Id }}-server.cert
+  key: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/keys/{{ .Component.Id }}-server.key
+  ca: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/certs/{{ .Component.Id }}-server.chain.pem
+
+commandRateLimiter:
+    enabled:   true
+    maxQueued: 25
+
+# the endpoint that routers will connect to the controller over.
+ctrl:
+  listener: tls:0.0.0.0:6262
+  options:
+    advertiseAddress: tls:{{ .Host.PublicIp }}:6262
+    # (optional) settings
+    # set the maximum number of connect requests that are buffered and waiting to be acknowledged (1 to 5000, default 1000)
+    #maxQueuedConnects:      50
+
+    # the maximum number of connects that have  begun hello synchronization (1 to 1000, default 16)
+    #maxOutstandingConnects: 100
+
+    # the number of milliseconds to wait before a hello synchronization fails and closes the connection (30ms to 60000ms, default: 1000ms)
+    #connectTimeoutMs:       3000
+
+    # Sets the control channel write timeout. A write timeout will close the control channel, so the router will reconnect
+    #writeTimeout: 15s
+
+    # A listener address which will be sent to connecting routers in order to change their configured controller
+    # address. If defined, routers will update address configuration to immediately use the new address for future
+    # connections. The value of newListener must be resolvable both via DNS and validate via certificates
+    #newListener: tls:localhost:6262
+
+events:
+  jsonLogger:
+    subscriptions:
+      - type: entityChange
+      - type: edge.apiSessions
+      - type: edge.entityCounts
+        interval: 15s
+      - type: edge.sessions
+      - type: fabric.routers
+      - type: fabric.terminators
+#      - type: metrics
+#        sourceFilter: .*
+#        metricFilter: .*egress.*m1_rate*
+#      - type: fabric.circuits
+#        include:
+#          - created
+#        include:
+#          - created
+#      - type: fabric.usage
+#      - type: services
+#      - type: fabric.usage
+    handler:
+      type: file
+      format: json
+      path: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/logs/event.log
+
+healthChecks:
+  boltCheck:
+    # How often to try entering a bolt read tx. Defaults to 30 seconds
+    interval: 30s
+    # When to timeout the check. Defaults to 15 seconds
+    timeout: 15s
+    # How long to wait before starting the check. Defaults to 15 seconds
+    initialDelay: 15s
+
+# By having an 'edge' section defined, the ziti-controller will attempt to parse the edge configuration. Removing this
+# section, commenting out, or altering the name of the section will cause the edge to not run.
+edge:
+  # This section represents the configuration of the Edge API that is served over HTTPS
+  api:
+    #(optional, default 90s) Alters how frequently heartbeat and last activity values are persisted
+    # activityUpdateInterval: 90s
+    #(optional, default 250) The number of API Sessions updated for last activity per transaction
+    # activityUpdateBatchSize: 250
+    # sessionTimeout - optional, default 10m
+    # The number of minutes before an Edge API session will timeout. Timeouts are reset by
+    # API requests and connections that are maintained to Edge Routers
+    sessionTimeout: 30m
+    # address - required
+    # The default address (host:port) to use for enrollment for the Client API. This value must match one of the addresses
+    # defined in a bind point's address field for the `edge-client` API in the web section.
+    address: {{ .Host.PublicIp }}:1280
+  # enrollment - required
+  # A section containing settings pertaining to enrollment.
+  enrollment:
+    # signingCert - required
+    # A Ziti Identity configuration section that specifically makes use of the cert and key fields to define
+    # a signing certificate from the PKI that the Ziti environment is using to sign certificates. The signingCert.cert
+    # will be added to the /.well-known CA store that is used to bootstrap trust with the Ziti Controller.
+    signingCert:
+      cert: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/certs/{{ .Component.Id }}.cert
+      key: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/keys/{{ .Component.Id }}.key
+
+    # edgeIdentity - optional
+    # A section for identity enrollment specific settings
+    edgeIdentity:
+      # duration - optional, default 5m
+      # The length of time that a Ziti Edge Identity enrollment should remain valid. After
+      # this duration, the enrollment will expire and not longer be usable.
+      duration: 1h
+    # edgeRouter - Optional
+    # A section for edge router enrollment specific settings.
+    edgeRouter:
+      # duration - optional, default 5m
+      # The length of time that a Ziti Edge Router enrollment should remain valid. After
+      # this duration, the enrollment will expire and not longer be usable.
+      duration: 1h
+
+
+# web - optional
+# Defines webListeners that will be hosted by the controller. Each webListener can host many APIs and be bound to many
+# bind points.
+web:
+  # name - required
+  # Provides a name for this listener, used for logging output. Not required to be unique, but is highly suggested.
+  - name: all-apis-localhost
+    # bindPoints - required
+    # One or more bind points are required. A bind point specifies an interface (interface:port string) that defines
+    # where on the host machine the webListener will listen and the address (host:port) that should be used to
+    # publicly address the webListener(i.e. mydomain.com, localhost, 127.0.0.1). This public address may be used for
+    # incoming address resolution as well as used in responses in the API.
+    bindPoints:
+      #interface - required
+      # A host:port string on which network interface to listen on. 0.0.0.0 will listen on all interfaces
+      - interface: 0.0.0.0:1280
+
+        # address - required
+        # The public address that external incoming requests will be able to resolve. Used in request processing and
+        # response content that requires full host:port/path addresses.
+        address: {{ .Host.PublicIp }}:1280
+
+        # newAddress - optional
+        # A host:port string which will be sent out as an HTTP header "ziti-new-address" if specified. If the header
+        # is present, clients should update location configuration to immediately use the new address for future
+        # connections. The value of newAddress must be resolvable both via DNS and validate via certificates
+        #newAddress: localhost:1280
+    # identity - optional
+    # Allows the webListener to have a specific identity instead of defaulting to the root `identity` section.
+    #    identity:
+    #      cert:                 ${ZITI_SOURCE}/ziti/etc/ca/intermediate/certs/ctrl-client.cert.pem
+    #      server_cert:          ${ZITI_SOURCE}/ziti/etc/ca/intermediate/certs/ctrl-server.cert.pem
+    #      key:                  ${ZITI_SOURCE}/ziti/etc/ca/intermediate/private/ctrl.key.pem
+    #      ca:                   ${ZITI_SOURCE}/ziti/etc/ca/intermediate/certs/ca-chain.cert.pem
+    # options - optional
+    # Allows the specification of webListener level options - mainly dealing with HTTP/TLS settings. These options are
+    # used for all http servers started by the current webListener.
+    options:
+      # idleTimeout - optional, default 5000ms
+      # The maximum amount of idle time in milliseconds allowed for pipelined HTTP requests. Setting this too high
+      # can cause resources on the host to be consumed as clients remain connected and idle. Lowering this value
+      # will cause clients to reconnect on subsequent HTTPs requests.
+      idleTimeout: 5000ms  #http timeouts, new
+
+      # readTimeout - optional, default 5000ms
+      # The maximum amount of time in milliseconds http servers will wait to read the first incoming requests. A higher
+      # value risks consuming resources on the host with clients that are acting bad faith or suffering from high latency
+      # or packet loss. A lower value can risk losing connections to high latency/packet loss clients.
+
+      readTimeout: 5000ms
+      # writeTimeout - optional, default 10000ms
+      # The total maximum time in milliseconds that the http server will wait for a single requests to be received and
+      # responded too. A higher value can allow long running requests to consume resources on the host. A lower value
+      # can risk ending requests before the server has a chance to respond.
+
+      writeTimeout: 100000ms
+      # minTLSVersion - optional, default TSL1.2
+      # The minimum version of TSL to support
+
+      minTLSVersion: TLS1.2
+      # maxTLSVersion - optional, default TSL1.3
+      # The maximum version of TSL to support
+
+      maxTLSVersion: TLS1.3
+    # apis - required
+    # Allows one or more APIs to be bound to this webListener
+    apis:
+      # binding - required
+      # Specifies an API to bind to this webListener. Built-in APIs are
+      #   - health-checks
+      #   - edge-management
+      #   - edge-client
+      #   - fabric-management
+      - binding: health-checks
+        options: {}
+      - binding: fabric
+      - binding: edge-management
+        # options - variable optional/required
+        # This section is used to define values that are specified by the API they are associated with.
+        # These settings are per API. The example below is for the `edge-api` and contains both optional values and
+        # required values.
+        options: {}
+      - binding: edge-client
+        options: {}
diff --git a/zititest/models/db-sdk-hosting-test/configs/router.yml.tmpl b/zititest/models/db-sdk-hosting-test/configs/router.yml.tmpl
new file mode 100644
index 000000000..72999dd24
--- /dev/null
+++ b/zititest/models/db-sdk-hosting-test/configs/router.yml.tmpl
@@ -0,0 +1,70 @@
+{{$ssh_username := .Model.MustVariable "credentials.ssh.username"}}
+{{$identity := .Component.Id}}
+{{$router_ip := .Host.PublicIp}}
+  
+v: 3
+
+enableDebugOps: true
+
+identity:
+  cert:                 /home/{{$ssh_username}}/fablab/cfg/{{$identity}}-client.cert
+  server_cert:          /home/{{$ssh_username}}/fablab/cfg/{{$identity}}-server.cert
+  key:                  /home/{{$ssh_username}}/fablab/cfg/{{$identity}}.key
+  ca:                   /home/{{$ssh_username}}/fablab/cfg/{{$identity}}-server.chain.pem
+
+ctrl:
+  endpoints: {{ range $host := .Model.MustSelectHosts "component.ctrl" 1 }}
+    - tls:{{ $host.PublicIp }}:6262{{end}}
+
+healthChecks:
+  ctrlPingCheck:
+    # How often to ping the controller over the control channel. Defaults to 30 seconds
+    interval: 30s
+    # When to timeout the ping. Defaults to 15 seconds
+    timeout: 15s
+    # How long to wait before pinging the controller. Defaults to 15 seconds
+    initialDelay: 15s
+
+metrics:
+  reportInterval: 15s
+  messageQueueSize: 10
+
+link:
+  listeners:
+    - binding:          transport
+      bind:             tls:0.0.0.0:6000
+      advertise:        tls:{{$router_ip}}:6000
+  dialers:
+    - binding:          transport
+
+listeners:
+{{if .Component.HasTag "tunneler"}}
+  - binding: tunnel
+    options:
+      mode: host
+{{end}}
+  - binding: edge
+    address: tls:0.0.0.0:6262
+    options:
+      # (required) The public hostname and port combination that Ziti SDKs should connect on. Previously this was in the chanIngress section.
+      advertise: {{ .Host.PublicIp }}:6262
+
+# By having an 'edge' section defined, the ziti-router will attempt to parse the edge configuration. Removing this
+# section, commenting out, or altering the name of the section will cause the router to no longer operate as an Edge
+# Router.
+edge:
+  # (required) Information used to generate the initial registration CSR. For documentation on these fields please
+  # refer to the openssl documentation. These values MUST be supplied and have no defaults.
+  csr:
+    country: US
+    province: NC
+    locality: Charlotte
+    organization: NetFoundry
+    organizationalUnit: Ziti
+
+    # (required) SANs that this Gateways certs should contain. At least one IP or DNS SAN should be defined that matches
+    # the edge listeners "advertise" value from the "listeners" section.
+    sans:
+      ip:
+        - {{ .Host.PublicIp }}
+
diff --git a/zititest/models/db-sdk-hosting-test/main.go b/zititest/models/db-sdk-hosting-test/main.go
new file mode 100644
index 000000000..9ff26d6a3
--- /dev/null
+++ b/zititest/models/db-sdk-hosting-test/main.go
@@ -0,0 +1,324 @@
+package main
+
+import (
+	"embed"
+	_ "embed"
+	"fmt"
+	"github.com/openziti/fablab"
+	"github.com/openziti/fablab/kernel/lib/actions"
+	"github.com/openziti/fablab/kernel/lib/actions/component"
+	"github.com/openziti/fablab/kernel/lib/actions/host"
+	"github.com/openziti/fablab/kernel/lib/actions/semaphore"
+	"github.com/openziti/fablab/kernel/lib/binding"
+	"github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/aws_ssh_key"
+	"github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/semaphore"
+	"github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/terraform"
+	distribution "github.com/openziti/fablab/kernel/lib/runlevel/3_distribution"
+	"github.com/openziti/fablab/kernel/lib/runlevel/3_distribution/rsync"
+	aws_ssh_key2 "github.com/openziti/fablab/kernel/lib/runlevel/6_disposal/aws_ssh_key"
+	"github.com/openziti/fablab/kernel/lib/runlevel/6_disposal/terraform"
+	"github.com/openziti/fablab/kernel/model"
+	"github.com/openziti/fablab/resources"
+	"github.com/openziti/ziti/controller/db"
+	"github.com/openziti/ziti/zititest/models/test_resources"
+	"github.com/openziti/ziti/zititest/zitilab"
+	"github.com/openziti/ziti/zititest/zitilab/actions/edge"
+	"github.com/openziti/ziti/zititest/zitilab/models"
+	"go.etcd.io/bbolt"
+	"os"
+	"path"
+	"strings"
+	"time"
+)
+
+// const TargetZitiVersion = "v0.31.0"
+
+const TargetZitiVersion = ""
+const TargetZitiEdgeTunnelVersion = ""
+
+//const TargetZitiEdgeTunnelVersion = "0.22.12"
+
+var TunnelType = "!zet"
+
+//go:embed configs
+var configResource embed.FS
+
+type dbStrategy struct{}
+
+func (d dbStrategy) GetDbFile(m *model.Model) string {
+	return m.MustStringVariable("db_file")
+}
+
+func (d dbStrategy) GetSite(router *db.EdgeRouter) (string, bool) {
+	if strings.Contains(strings.ToLower(router.Name), "london") {
+		return "eu-west-2a", true // london region
+	}
+	if strings.Contains(strings.ToLower(router.Name), "virginia") {
+		return "us-east-1a", true // london region
+	}
+	if strings.Contains(strings.ToLower(router.Name), "melbourne") {
+		return "ap-southeast-2a", true // sydney region
+	}
+
+	return "us-east-1a", true
+}
+
+func (d dbStrategy) PostProcess(router *db.EdgeRouter, c *model.Component) {
+	if router.IsTunnelerEnabled {
+		c.Scope.Tags = append(c.Scope.Tags, "tunneler")
+	}
+	c.Scope.Tags = append(c.Scope.Tags, "edge-router")
+	c.Scope.Tags = append(c.Scope.Tags, "pre-created")
+	c.Host.InstanceType = "c5.xlarge"
+	c.Type.(*zitilab.RouterType).Version = TargetZitiVersion
+}
+
+func (d dbStrategy) ProcessDbModel(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error {
+	if err := builder.CreateEdgeRouterHosts(tx, m); err != nil {
+		return err
+	}
+	return d.CreateIdentityHosts(tx, m, builder)
+}
+
+func (d dbStrategy) CreateIdentityHosts(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error {
+	stores := builder.GetStores()
+	ids, _, err := stores.Identity.QueryIds(tx, "true limit none")
+	if err != nil {
+		return err
+	}
+
+	servicesCount := 0
+	hostingIdentities := map[string]int{}
+
+	for _, identityId := range ids {
+		cursorProvider := stores.Identity.GetIdentityServicesCursorProvider(identityId)
+		cursor := cursorProvider(tx, true)
+		identityServiceCount := 0
+		for cursor.IsValid() {
+			serviceId := string(cursor.Current())
+			if stores.EdgeService.IsBindableByIdentity(tx, serviceId, identityId) {
+				identityServiceCount++
+			}
+			cursor.Next()
+		}
+		if identityServiceCount > 0 {
+			servicesCount += identityServiceCount
+			hostingIdentities[identityId] = identityServiceCount
+		}
+	}
+
+	fmt.Printf("service count: %v\n", servicesCount)
+
+	regionCount := len(m.Regions)
+
+	perRegion := servicesCount / regionCount
+	idIdx := 0
+
+	avgTunnelsPerHost := 15
+
+	m.RangeSortedRegions(func(regionId string, region *model.Region) {
+		regionServiceCount := 0
+
+		var regionIdentityIds []string
+
+		for {
+			if idIdx >= len(ids) {
+				break
+			}
+			identityId := ids[idIdx]
+			idIdx++
+
+			svcCount, found := hostingIdentities[identityId]
+			if !found {
+				continue
+			}
+			regionServiceCount += svcCount
+			regionIdentityIds = append(regionIdentityIds, identityId)
+			if regionServiceCount > perRegion {
+				break
+			}
+		}
+
+		hostCount := len(regionIdentityIds) / avgTunnelsPerHost
+		var hosts []*model.Host
+
+		for i := 0; i < hostCount; i++ {
+			tunnelsHost := &model.Host{
+				Scope:        model.Scope{Tags: model.Tags{}},
+				Region:       region,
+				Components:   model.Components{},
+				InstanceType: "t3.xlarge",
+			}
+			hostId := fmt.Sprintf("%s_svc_hosts_%v", regionId, i)
+			region.Hosts[hostId] = tunnelsHost
+			hosts = append(hosts, tunnelsHost)
+		}
+
+		hostIdx := 0
+		for _, identityId := range regionIdentityIds {
+			tunnelHost := hosts[hostIdx%len(hosts)]
+			hostIdx++
+
+			svcCount := hostingIdentities[identityId]
+
+			getConfigPath := func(c *model.Component) string {
+				user := c.GetHost().GetSshUser()
+				return fmt.Sprintf("/home/%s/etc/%s.json", user, c.Id)
+			}
+
+			var tunnelType model.ComponentType
+			if TunnelType == "zet" {
+				tunnelType = &zitilab.ZitiEdgeTunnelType{
+					Version:     TargetZitiEdgeTunnelVersion,
+					LogConfig:   "'2;bind.c=6'",
+					ConfigPathF: getConfigPath,
+				}
+			} else {
+				tunnelType = &zitilab.ZitiTunnelType{
+					Mode:        zitilab.ZitiTunnelModeHost,
+					Version:     TargetZitiVersion,
+					ConfigPathF: getConfigPath,
+				}
+			}
+
+			tunnelComponent := &model.Component{
+				Scope: model.Scope{Tags: model.Tags{"client", "pre-created", fmt.Sprintf("serviceCount=%v", svcCount)}},
+				Type:  tunnelType,
+				Host:  tunnelHost,
+			}
+			tunnelHost.Components[identityId] = tunnelComponent
+		}
+	})
+
+	return nil
+}
+
+var dbStrategyInstance = dbStrategy{}
+
+var m = &model.Model{
+	Id: "db-sdk-hosting-test",
+	Scope: model.Scope{
+		Defaults: model.Variables{
+			"environment": "db-sdk-hosting-test",
+			"credentials": model.Variables{
+				"aws": model.Variables{
+					"managed_key": true,
+				},
+				"ssh": model.Variables{
+					"username": "ubuntu",
+				},
+				"edge": model.Variables{
+					"username": "admin",
+					"password": "admin",
+				},
+			},
+			"metrics": model.Variables{
+				"influxdb": model.Variables{
+					"url": "http://localhost:8086",
+					"db":  "ziti",
+				},
+			},
+		},
+	},
+	StructureFactories: []model.Factory{
+		&models.ZitiDbBuilder{Strategy: dbStrategyInstance},
+	},
+	Resources: model.Resources{
+		resources.Configs:   resources.SubFolder(configResource, "configs"),
+		resources.Binaries:  os.DirFS(path.Join(os.Getenv("GOPATH"), "bin")),
+		resources.Terraform: test_resources.TerraformResources(),
+	},
+	Regions: model.Regions{
+		"us-east-1": {
+			Region: "us-east-1",
+			Site:   "us-east-1a",
+			Hosts: model.Hosts{
+				"ctrl": {
+					InstanceType: "c5.xlarge",
+					Components: model.Components{
+						"ctrl": {
+							Scope: model.Scope{Tags: model.Tags{"ctrl"}},
+							Type: &zitilab.ControllerType{
+								Version: TargetZitiVersion,
+							},
+						},
+					},
+				},
+			},
+		},
+	},
+
+	Actions: model.ActionBinders{
+		"bootstrap": model.ActionBinder(func(m *model.Model) model.Action {
+			workflow := actions.Workflow()
+
+			workflow.AddAction(component.Start("#ctrl"))
+			workflow.AddAction(semaphore.Sleep(2 * time.Second))
+
+			workflow.AddAction(edge.Login("#ctrl"))
+
+			workflow.AddAction(edge.ReEnrollEdgeRouters(".edge-router .pre-created", 2))
+			if quickRun, _ := m.GetBoolVariable("quick_run"); !quickRun {
+				workflow.AddAction(edge.ReEnrollIdentities(".client .pre-created", 10))
+			}
+			return workflow
+		}),
+		"stop": model.Bind(component.StopInParallelHostExclusive("*", 15)),
+		"clean": model.Bind(actions.Workflow(
+			component.StopInParallelHostExclusive("*", 15),
+			host.GroupExec("*", 25, "rm -f logs/*"),
+		)),
+		"login": model.Bind(edge.Login("#ctrl")),
+		"restart": model.ActionBinder(func(run *model.Model) model.Action {
+			workflow := actions.Workflow()
+			workflow.AddAction(component.StopInParallel("*", 100))
+			workflow.AddAction(component.Start(".ctrl"))
+			workflow.AddAction(semaphore.Sleep(2 * time.Second))
+			workflow.AddAction(component.StartInParallel(".edge-router", 10))
+			workflow.AddAction(semaphore.Sleep(2 * time.Second))
+			workflow.AddAction(component.StartInParallel(".client", 50))
+			return workflow
+		}),
+	},
+
+	Infrastructure: model.Stages{
+		aws_ssh_key.Express(),
+		&terraform_0.Terraform{
+			Retries: 3,
+			ReadyCheck: &semaphore_0.ReadyStage{
+				MaxWait: 90 * time.Second,
+			},
+		},
+	},
+
+	Distribution: model.Stages{
+		distribution.DistributeSshKey("*"),
+		rsync.RsyncStaged(),
+		model.StageActionF(func(run model.Run) error {
+			quickRun, _ := run.GetModel().GetBoolVariable("quick_run")
+			_, targetedSync := run.GetModel().Scope.GetVariable("sync.target")
+
+			if !quickRun && !targetedSync {
+				dbFile := dbStrategyInstance.GetDbFile(run.GetModel())
+				deferred := rsync.NewRsyncHost("#ctrl", dbFile, "/home/ubuntu/ctrl.db")
+				return deferred.Execute(run)
+			}
+			return nil
+		}),
+	},
+
+	Disposal: model.Stages{
+		terraform.Dispose(),
+		aws_ssh_key2.Dispose(),
+	},
+}
+
+func main() {
+	m.AddActivationActions("stop", "bootstrap")
+
+	model.AddBootstrapExtension(binding.AwsCredentialsLoader)
+	model.AddBootstrapExtension(aws_ssh_key.KeyManager)
+
+	fablab.InitModel(m)
+	fablab.Run()
+}
diff --git a/zititest/models/links-test/main.go b/zititest/models/links-test/main.go
index 86d892b3e..09d9207e0 100644
--- a/zititest/models/links-test/main.go
+++ b/zititest/models/links-test/main.go
@@ -116,7 +116,6 @@ var m = &model.Model{
 			Site:   "us-east-1a",
 			Hosts: model.Hosts{
 				"ctrl1": {
-					InstanceType: "t3.medium",
 					Components: model.Components{
 						"ctrl1": {
 							Scope: model.Scope{Tags: model.Tags{"ctrl"}},
diff --git a/zititest/models/sdk-hosting-test/configs/ctrl.yml.tmpl b/zititest/models/sdk-hosting-test/configs/ctrl.yml.tmpl
index 5a134a115..850bbc302 100644
--- a/zititest/models/sdk-hosting-test/configs/ctrl.yml.tmpl
+++ b/zititest/models/sdk-hosting-test/configs/ctrl.yml.tmpl
@@ -13,6 +13,10 @@ identity:
   key: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/keys/{{ .Component.Id }}-server.key
   ca: /home/{{ .Model.MustVariable "credentials.ssh.username" }}/fablab/pki/{{ .Component.Id }}/certs/{{ .Component.Id }}-server.chain.pem
 
+commandRateLimiter:
+    enabled:   true
+    maxQueued: 25
+
 # the endpoint that routers will connect to the controller over.
 ctrl:
   listener: tls:0.0.0.0:6262
diff --git a/zititest/models/sdk-hosting-test/main.go b/zititest/models/sdk-hosting-test/main.go
index 175660e99..b1d173b01 100644
--- a/zititest/models/sdk-hosting-test/main.go
+++ b/zititest/models/sdk-hosting-test/main.go
@@ -3,14 +3,15 @@ package main
 import (
 	"embed"
 	_ "embed"
-	"errors"
 	"fmt"
+	"github.com/michaelquigley/pfxlog"
 	"github.com/openziti/fablab"
 	"github.com/openziti/fablab/kernel/lib/actions"
 	"github.com/openziti/fablab/kernel/lib/actions/component"
 	"github.com/openziti/fablab/kernel/lib/actions/host"
 	"github.com/openziti/fablab/kernel/lib/actions/semaphore"
 	"github.com/openziti/fablab/kernel/lib/binding"
+	"github.com/openziti/fablab/kernel/lib/parallel"
 	"github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/aws_ssh_key"
 	"github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/semaphore"
 	"github.com/openziti/fablab/kernel/lib/runlevel/0_infrastructure/terraform"
@@ -20,16 +21,15 @@ import (
 	"github.com/openziti/fablab/kernel/lib/runlevel/6_disposal/terraform"
 	"github.com/openziti/fablab/kernel/model"
 	"github.com/openziti/fablab/resources"
-	"github.com/openziti/ziti/controller/db"
 	"github.com/openziti/ziti/zititest/models/test_resources"
 	"github.com/openziti/ziti/zititest/zitilab"
+	zitilib_actions "github.com/openziti/ziti/zititest/zitilab/actions"
 	"github.com/openziti/ziti/zititest/zitilab/actions/edge"
+	"github.com/openziti/ziti/zititest/zitilab/chaos"
+	"github.com/openziti/ziti/zititest/zitilab/cli"
 	"github.com/openziti/ziti/zititest/zitilab/models"
-	"go.etcd.io/bbolt"
 	"os"
-	"os/exec"
 	"path"
-	"strings"
 	"time"
 )
 
@@ -45,158 +45,34 @@ var TunnelType = "!zet"
 //go:embed configs
 var configResource embed.FS
 
-type dbStrategy struct{}
+type scaleStrategy struct{}
 
-func (d dbStrategy) GetDbFile(m *model.Model) string {
-	return m.MustStringVariable("db_file")
-}
-
-func (d dbStrategy) GetSite(router *db.EdgeRouter) (string, bool) {
-	if strings.Contains(strings.ToLower(router.Name), "london") {
-		return "eu-west-2a", true // london region
-	}
-	if strings.Contains(strings.ToLower(router.Name), "virginia") {
-		return "us-east-1a", true // london region
-	}
-	if strings.Contains(strings.ToLower(router.Name), "melbourne") {
-		return "ap-southeast-2a", true // sydney region
-	}
-
-	return "us-east-1a", true
-}
-
-func (d dbStrategy) PostProcess(router *db.EdgeRouter, c *model.Component) {
-	if router.IsTunnelerEnabled {
-		c.Scope.Tags = append(c.Scope.Tags, "tunneler")
-	}
-	c.Scope.Tags = append(c.Scope.Tags, "edge-router")
-	c.Scope.Tags = append(c.Scope.Tags, "pre-created")
-	c.Host.InstanceType = "c5.xlarge"
-	c.Type.(*zitilab.RouterType).Version = TargetZitiVersion
-}
-
-func (d dbStrategy) ProcessDbModel(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error {
-	if err := builder.CreateEdgeRouterHosts(tx, m); err != nil {
-		return err
+func (self scaleStrategy) IsScaled(entity model.Entity) bool {
+	if entity.GetType() == model.EntityTypeHost {
+		return entity.GetScope().HasTag("router") || entity.GetScope().HasTag("host")
 	}
-	return d.CreateIdentityHosts(tx, m, builder)
+	return entity.GetType() == model.EntityTypeComponent && entity.GetScope().HasTag("host")
 }
 
-func (d dbStrategy) CreateIdentityHosts(tx *bbolt.Tx, m *model.Model, builder *models.ZitiDbBuilder) error {
-	stores := builder.GetStores()
-	ids, _, err := stores.Identity.QueryIds(tx, "true limit none")
-	if err != nil {
-		return err
-	}
-
-	servicesCount := 0
-	hostingIdentities := map[string]int{}
-
-	for _, identityId := range ids {
-		cursorProvider := stores.Identity.GetIdentityServicesCursorProvider(identityId)
-		cursor := cursorProvider(tx, true)
-		identityServiceCount := 0
-		for cursor.IsValid() {
-			serviceId := string(cursor.Current())
-			if stores.EdgeService.IsBindableByIdentity(tx, serviceId, identityId) {
-				identityServiceCount++
-			}
-			cursor.Next()
+func (self scaleStrategy) GetEntityCount(entity model.Entity) uint32 {
+	if entity.GetType() == model.EntityTypeHost {
+		if entity.GetScope().HasTag("router") {
+			return 2
 		}
-		if identityServiceCount > 0 {
-			servicesCount += identityServiceCount
-			hostingIdentities[identityId] = identityServiceCount
-		}
-	}
-
-	fmt.Printf("service count: %v\n", servicesCount)
-
-	regionCount := len(m.Regions)
-
-	perRegion := servicesCount / regionCount
-	idIdx := 0
-
-	avgTunnelsPerHost := 15
-
-	m.RangeSortedRegions(func(regionId string, region *model.Region) {
-		regionServiceCount := 0
-
-		var regionIdentityIds []string
-
-		for {
-			if idIdx >= len(ids) {
-				break
-			}
-			identityId := ids[idIdx]
-			idIdx++
-
-			svcCount, found := hostingIdentities[identityId]
-			if !found {
-				continue
-			}
-			regionServiceCount += svcCount
-			regionIdentityIds = append(regionIdentityIds, identityId)
-			if regionServiceCount > perRegion {
-				break
+		if entity.GetScope().HasTag("host") {
+			h := entity.(*model.Host)
+			if h.Region.Id == "us-east-1" {
+				return 8
 			}
+			return 6
 		}
-
-		hostCount := len(regionIdentityIds) / avgTunnelsPerHost
-		var hosts []*model.Host
-
-		for i := 0; i < hostCount; i++ {
-			tunnelsHost := &model.Host{
-				Scope:        model.Scope{Tags: model.Tags{}},
-				Region:       region,
-				Components:   model.Components{},
-				InstanceType: "t3.xlarge",
-			}
-			hostId := fmt.Sprintf("%s_svc_hosts_%v", regionId, i)
-			region.Hosts[hostId] = tunnelsHost
-			hosts = append(hosts, tunnelsHost)
-		}
-
-		hostIdx := 0
-		for _, identityId := range regionIdentityIds {
-			tunnelHost := hosts[hostIdx%len(hosts)]
-			hostIdx++
-
-			svcCount := hostingIdentities[identityId]
-
-			getConfigPath := func(c *model.Component) string {
-				user := c.GetHost().GetSshUser()
-				return fmt.Sprintf("/home/%s/etc/%s.json", user, c.Id)
-			}
-
-			var tunnelType model.ComponentType
-			if TunnelType == "zet" {
-				tunnelType = &zitilab.ZitiEdgeTunnelType{
-					Version:     TargetZitiEdgeTunnelVersion,
-					LogConfig:   "'2;bind.c=6'",
-					ConfigPathF: getConfigPath,
-				}
-			} else {
-				tunnelType = &zitilab.ZitiTunnelType{
-					Mode:        zitilab.ZitiTunnelModeHost,
-					Version:     TargetZitiVersion,
-					ConfigPathF: getConfigPath,
-				}
-			}
-
-			tunnelComponent := &model.Component{
-				Scope: model.Scope{Tags: model.Tags{"sdk-tunneler", "pre-created", fmt.Sprintf("serviceCount=%v", svcCount)}},
-				Type:  tunnelType,
-				Host:  tunnelHost,
-			}
-			tunnelHost.Components[identityId] = tunnelComponent
-		}
-	})
-
-	return nil
+	}
+	if entity.GetType() == model.EntityTypeComponent {
+		return 10
+	}
+	return 1
 }
 
-var dbStrategyInstance = dbStrategy{}
-
 var m = &model.Model{
 	Id: "sdk-hosting-test",
 	Scope: model.Scope{
@@ -223,7 +99,31 @@ var m = &model.Model{
 		},
 	},
 	StructureFactories: []model.Factory{
-		&models.ZitiDbBuilder{Strategy: dbStrategyInstance},
+		model.FactoryFunc(func(m *model.Model) error {
+			err := m.ForEachHost("component.router", 1, func(host *model.Host) error {
+				host.InstanceType = "c5.xlarge"
+				return nil
+			})
+
+			if err != nil {
+				return err
+			}
+
+			err = m.ForEachComponent(".host", 1, func(c *model.Component) error {
+				c.Type.(*zitilab.ZitiTunnelType).Mode = zitilab.ZitiTunnelModeHost
+				return nil
+			})
+
+			if err != nil {
+				return err
+			}
+
+			return m.ForEachHost("component.host", 1, func(host *model.Host) error {
+				host.InstanceType = "c5.xlarge"
+				return nil
+			})
+		}),
+		model.NewScaleFactoryWithDefaultEntityFactory(&scaleStrategy{}),
 	},
 	Resources: model.Resources{
 		resources.Configs:   resources.SubFolder(configResource, "configs"),
@@ -235,10 +135,10 @@ var m = &model.Model{
 			Region: "us-east-1",
 			Site:   "us-east-1a",
 			Hosts: model.Hosts{
-				"ctrl": {
+				"ctrl1": {
 					InstanceType: "c5.xlarge",
 					Components: model.Components{
-						"ctrl": {
+						"ctrl1": {
 							Scope: model.Scope{Tags: model.Tags{"ctrl"}},
 							Type: &zitilab.ControllerType{
 								Version: TargetZitiVersion,
@@ -246,6 +146,84 @@ var m = &model.Model{
 						},
 					},
 				},
+				"router-us-{{.ScaleIndex}}": {
+					Scope: model.Scope{Tags: model.Tags{"router"}},
+					Components: model.Components{
+						"router-us-{{.Host.ScaleIndex}}": {
+							Scope: model.Scope{Tags: model.Tags{"router"}},
+							Type: &zitilab.RouterType{
+								Version: TargetZitiVersion,
+							},
+						},
+					},
+				},
+				"host-us-{{ .ScaleIndex }}": {
+					Scope: model.Scope{Tags: model.Tags{"host"}},
+					Components: model.Components{
+						"host-us-{{ .Host.ScaleIndex }}-{{ .ScaleIndex }}": {
+							Scope: model.Scope{Tags: model.Tags{"host"}},
+							Type: &zitilab.ZitiTunnelType{
+								Version: TargetZitiVersion,
+							},
+						},
+					},
+				},
+			},
+		},
+		"eu-west-2": {
+			Region: "us-west-2",
+			Site:   "us-west-2a",
+			Hosts: model.Hosts{
+				"router-eu-{{.ScaleIndex}}": {
+					Scope: model.Scope{Tags: model.Tags{"router"}},
+					Components: model.Components{
+						"router-eu-{{.Host.ScaleIndex}}": {
+							Scope: model.Scope{Tags: model.Tags{"router"}},
+							Type: &zitilab.RouterType{
+								Version: TargetZitiVersion,
+							},
+						},
+					},
+				},
+				"host-eu-{{ .ScaleIndex }}": {
+					Scope: model.Scope{Tags: model.Tags{"host"}},
+					Components: model.Components{
+						"host-eu-{{ .Host.ScaleIndex }}-{{ .ScaleIndex }}": {
+							Scope: model.Scope{Tags: model.Tags{"host"}},
+							Type: &zitilab.ZitiTunnelType{
+								Version: TargetZitiVersion,
+							},
+						},
+					},
+				},
+			},
+		},
+		"ap-southeast-2": {
+			Region: "ap-southeast-2",
+			Site:   "ap-southeast-2a",
+			Hosts: model.Hosts{
+				"router-ap-{{.ScaleIndex}}": {
+					Scope: model.Scope{Tags: model.Tags{"router", "scaled"}},
+					Components: model.Components{
+						"router-ap-{{.Host.ScaleIndex}}": {
+							Scope: model.Scope{Tags: model.Tags{"router"}},
+							Type: &zitilab.RouterType{
+								Version: TargetZitiVersion,
+							},
+						},
+					},
+				},
+				"host-ap-{{ .ScaleIndex }}": {
+					Scope: model.Scope{Tags: model.Tags{"host", "scaled"}},
+					Components: model.Components{
+						"host-ap-{{ .Host.ScaleIndex }}-{{ .ScaleIndex }}": {
+							Scope: model.Scope{Tags: model.Tags{"host"}},
+							Type: &zitilab.ZitiTunnelType{
+								Version: TargetZitiVersion,
+							},
+						},
+					},
+				},
 			},
 		},
 	},
@@ -254,15 +232,80 @@ var m = &model.Model{
 		"bootstrap": model.ActionBinder(func(m *model.Model) model.Action {
 			workflow := actions.Workflow()
 
-			workflow.AddAction(component.Start("#ctrl"))
-			workflow.AddAction(semaphore.Sleep(2 * time.Second))
+			isHA := len(m.SelectComponents(".ctrl")) > 1
+
+			workflow.AddAction(component.StopInParallel("*", 300))
+			workflow.AddAction(host.GroupExec("*", 25, "rm -f logs/* ctrl.db"))
+			workflow.AddAction(host.GroupExec("component.ctrl", 5, "rm -rf ./fablab/ctrldata"))
+
+			if !isHA {
+				workflow.AddAction(component.Exec("#ctrl1", zitilab.ControllerActionInitStandalone))
+			}
 
-			workflow.AddAction(edge.Login("#ctrl"))
+			workflow.AddAction(component.Start(".ctrl"))
 
-			workflow.AddAction(edge.ReEnrollEdgeRouters(".edge-router .pre-created", 2))
-			if quickRun, _ := m.GetBoolVariable("quick_run"); !quickRun {
-				workflow.AddAction(edge.ReEnrollIdentities(".sdk-tunneler .pre-created", 10))
+			if isHA {
+				workflow.AddAction(semaphore.Sleep(2 * time.Second))
+				workflow.AddAction(edge.RaftJoin(".ctrl"))
+				workflow.AddAction(semaphore.Sleep(2 * time.Second))
+				workflow.AddAction(edge.InitRaftController("#ctrl1"))
 			}
+
+			workflow.AddAction(edge.ControllerAvailable("#ctrl1", 30*time.Second))
+
+			workflow.AddAction(edge.Login("#ctrl1"))
+
+			workflow.AddAction(edge.InitEdgeRouters(models.RouterTag, 25))
+			workflow.AddAction(edge.InitIdentities(".host", 25))
+
+			workflow.AddAction(zitilib_actions.Edge("create", "edge-router-policy", "all", "--edge-router-roles", "#all", "--identity-roles", "#all"))
+			workflow.AddAction(zitilib_actions.Edge("create", "service-edge-router-policy", "all", "--service-roles", "#all", "--edge-router-roles", "#all"))
+
+			workflow.AddAction(zitilib_actions.Edge("create", "config", "host-config", "host.v1", `
+				{
+					"address" : "localhost",
+					"port" : 8080,
+					"protocol" : "tcp"
+				}`))
+
+			workflow.AddAction(model.ActionFunc(func(run model.Run) error {
+				var tasks []parallel.Task
+				for i := 0; i < 2000; i++ {
+					name := fmt.Sprintf("service-%04d", i)
+					task := func() error {
+						_, err := cli.Exec(run.GetModel(), "edge", "create", "service", name, "-c", "host-config")
+						return err
+					}
+					tasks = append(tasks, task)
+				}
+				return parallel.Execute(tasks, 25)
+			}))
+
+			workflow.AddAction(model.ActionFunc(func(run model.Run) error {
+				identities := getHostNames()
+				serviceIdx := 0
+				var tasks []parallel.Task
+				for i, identity := range identities {
+					name := fmt.Sprintf("service-policy-%03d", i)
+					identityRoles := fmt.Sprintf("@%s", identity)
+					servicesRoles := ""
+					for j := 0; j < 10; j++ {
+						idx := serviceIdx % 2000
+						if j > 0 {
+							servicesRoles += ","
+						}
+						servicesRoles += fmt.Sprintf("@service-%04d", idx)
+						serviceIdx++
+					}
+					tasks = append(tasks, func() error {
+						_, err := cli.Exec(run.GetModel(), "edge", "create", "service-policy", name, "Bind",
+							"--identity-roles", identityRoles, "--service-roles", servicesRoles)
+						return err
+					})
+				}
+				return parallel.Execute(tasks, 25)
+			}))
+
 			return workflow
 		}),
 		"stop": model.Bind(component.StopInParallelHostExclusive("*", 15)),
@@ -270,45 +313,36 @@ var m = &model.Model{
 			component.StopInParallelHostExclusive("*", 15),
 			host.GroupExec("*", 25, "rm -f logs/*"),
 		)),
-		"login": model.Bind(edge.Login("#ctrl")),
-		"refreshCtrlZiti": model.ActionBinder(func(m *model.Model) model.Action {
-			return model.ActionFunc(func(run model.Run) error {
-				zitiPath, err := exec.LookPath("ziti")
-				if err != nil {
-					return err
-				}
-
-				deferred := rsync.NewRsyncHost("ctrl", zitiPath, "/home/ubuntu/fablab/bin/ziti")
-				return deferred.Execute(run)
-			})
-		}),
-		"refreshRouterZiti": model.ActionBinder(func(m *model.Model) model.Action {
-			return model.ActionFunc(func(run model.Run) error {
-				zitiPath, err := exec.LookPath("ziti")
-				if err != nil {
-					return err
-				}
-
-				deferred := rsync.NewRsyncHost("component.edge-router", zitiPath, "/home/ubuntu/fablab/bin/ziti")
-				return deferred.Execute(run)
-			})
+		"login": model.Bind(edge.Login("#ctrl1")),
+		"restart": model.ActionBinder(func(run *model.Model) model.Action {
+			workflow := actions.Workflow()
+			workflow.AddAction(component.StopInParallel("*", 100))
+			workflow.AddAction(host.GroupExec("*", 25, "rm -f logs/*"))
+			workflow.AddAction(component.Start(".ctrl"))
+			workflow.AddAction(semaphore.Sleep(2 * time.Second))
+			workflow.AddAction(component.StartInParallel(".router", 10))
+			workflow.AddAction(semaphore.Sleep(2 * time.Second))
+			workflow.AddAction(component.StartInParallel(".host", 50))
+			return workflow
 		}),
-		"refreshZiti": model.ActionBinder(func(m *model.Model) model.Action {
-			return model.ActionFunc(func(run model.Run) error {
-				zitiPath, err := exec.LookPath("ziti")
-				if err != nil {
-					return err
-				}
-
-				hosts := os.Getenv("HOSTS")
-				if hosts == "" {
-					return errors.New("expected hosts to refresh in HOSTS env")
-				}
-
-				deferred := rsync.NewRsyncHost(hosts, zitiPath, "/home/ubuntu/fablab/bin/ziti")
-				return deferred.Execute(run)
+		"sowChaos": model.Bind(model.ActionFunc(sowChaos)),
+		"validateUp": model.Bind(model.ActionFunc(func(run model.Run) error {
+			if err := chaos.ValidateUp(run, ".ctrl", 3, 15*time.Second); err != nil {
+				return err
+			}
+			err := run.GetModel().ForEachComponent(".ctrl", 3, func(c *model.Component) error {
+				return edge.ControllerAvailable(c.Id, 30*time.Second).Execute(run)
 			})
-		}),
+			if err != nil {
+				return err
+			}
+			if err := chaos.ValidateUp(run, ".router", 100, time.Minute); err != nil {
+				pfxlog.Logger().WithError(err).Error("validate up failed, trying to start all routers again")
+				return component.StartInParallel(".router", 100).Execute(run)
+			}
+			return nil
+		})),
+		"validate": model.Bind(model.ActionFunc(validateTerminators)),
 	},
 
 	Infrastructure: model.Stages{
@@ -324,14 +358,6 @@ var m = &model.Model{
 	Distribution: model.Stages{
 		distribution.DistributeSshKey("*"),
 		rsync.RsyncStaged(),
-		model.StageActionF(func(run model.Run) error {
-			if quickRun, _ := run.GetModel().GetBoolVariable("quick_run"); !quickRun {
-				dbFile := dbStrategyInstance.GetDbFile(run.GetModel())
-				deferred := rsync.NewRsyncHost("#ctrl", dbFile, "/home/ubuntu/ctrl.db")
-				return deferred.Execute(run)
-			}
-			return nil
-		}),
 	},
 
 	Disposal: model.Stages{
@@ -340,6 +366,20 @@ var m = &model.Model{
 	},
 }
 
+func getHostNames() []string {
+	var result []string
+	for i := 0; i < 8; i++ {
+		for j := 0; j < 10; j++ {
+			result = append(result, fmt.Sprintf("host-us-%d-%d", i, j))
+			if i < 6 {
+				result = append(result, fmt.Sprintf("host-eu-%d-%d", i, j))
+				result = append(result, fmt.Sprintf("host-ap-%d-%d", i, j))
+			}
+		}
+	}
+	return result
+}
+
 func main() {
 	m.AddActivationActions("stop", "bootstrap")
 
diff --git a/zititest/models/sdk-hosting-test/validation.go b/zititest/models/sdk-hosting-test/validation.go
new file mode 100644
index 000000000..f1fcc4ac6
--- /dev/null
+++ b/zititest/models/sdk-hosting-test/validation.go
@@ -0,0 +1,263 @@
+/*
+	Copyright NetFoundry Inc.
+
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+	https://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/michaelquigley/pfxlog"
+	"github.com/openziti/channel/v2"
+	"github.com/openziti/channel/v2/protobufs"
+	"github.com/openziti/fablab/kernel/model"
+	"github.com/openziti/ziti/common/pb/mgmt_pb"
+	"github.com/openziti/ziti/controller/rest_client/terminator"
+	"github.com/openziti/ziti/zititest/zitilab/chaos"
+	"github.com/openziti/ziti/zititest/zitilab/zitirest"
+	"google.golang.org/protobuf/proto"
+	"math/rand"
+	"time"
+)
+
+// start with a random scenario then cycle through them
+var scenarioCounter = rand.Intn(7)
+
+func sowChaos(run model.Run) error {
+	var controllers []*model.Component
+	var err error
+
+	scenarioCounter = (scenarioCounter + 1) % 7
+	scenario := scenarioCounter + 1
+
+	if scenario&0b001 > 0 {
+		controllers, err = chaos.SelectRandom(run, ".ctrl", chaos.RandomOfTotal())
+		if err != nil {
+			return err
+		}
+		time.Sleep(5 * time.Second)
+	}
+
+	var routers []*model.Component
+	if scenario&0b010 > 0 {
+		routers, err = chaos.SelectRandom(run, ".router", chaos.PercentageRange(10, 75))
+		if err != nil {
+			return err
+		}
+	}
+
+	var hosts []*model.Component
+	if scenario&0b100 > 0 {
+		hosts, err = chaos.SelectRandom(run, ".host", chaos.PercentageRange(10, 75))
+		if err != nil {
+			return err
+		}
+	}
+
+	toRestart := append(([]*model.Component)(nil), controllers...)
+	toRestart = append(toRestart, routers...)
+	toRestart = append(toRestart, hosts...)
+	fmt.Printf("restarting %d controllers,  %d routers and %d hosts\n", len(controllers), len(routers), len(hosts))
+	return chaos.RestartSelected(run, toRestart, 100)
+}
+
+func validateTerminators(run model.Run) error {
+	ctrls := run.GetModel().SelectComponents(".ctrl")
+	errC := make(chan error, len(ctrls))
+	deadline := time.Now().Add(15 * time.Minute)
+	for _, ctrl := range ctrls {
+		ctrlComponent := ctrl
+		go validateTerminatorsForCtrlWithChan(ctrlComponent, deadline, errC)
+	}
+
+	for i := 0; i < len(ctrls); i++ {
+		err := <-errC
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func validateTerminatorsForCtrlWithChan(c *model.Component, deadline time.Time, errC chan<- error) {
+	errC <- validateTerminatorsForCtrl(c, deadline)
+}
+
+func validateTerminatorsForCtrl(c *model.Component, deadline time.Time) error {
+	expectedTerminatorCount := int64(6000)
+	username := c.MustStringVariable("credentials.edge.username")
+	password := c.MustStringVariable("credentials.edge.password")
+	edgeApiBaseUrl := c.Host.PublicIp + ":1280"
+
+	var clients *zitirest.Clients
+	loginStart := time.Now()
+	for {
+		var err error
+		clients, err = zitirest.NewManagementClients(edgeApiBaseUrl)
+		if err != nil {
+			if time.Since(loginStart) > time.Minute {
+				return err
+			}
+			pfxlog.Logger().WithField("ctrlId", c.Id).WithError(err).Info("failed to initialize mgmt client, trying again in 1s")
+			time.Sleep(time.Second)
+			continue
+		}
+
+		if err = clients.Authenticate(username, password); err != nil {
+			if time.Since(loginStart) > time.Minute {
+				return err
+			}
+			pfxlog.Logger().WithField("ctrlId", c.Id).WithError(err).Info("failed to authenticate, trying again in 1s")
+			time.Sleep(time.Second)
+		} else {
+			break
+		}
+	}
+
+	terminatorsPresent := false
+	start := time.Now()
+
+	logger := pfxlog.Logger().WithField("ctrl", c.Id)
+	var lastLog time.Time
+	for time.Now().Before(deadline) && !terminatorsPresent {
+		terminatorCount, err := getTerminatorCount(clients)
+		if err != nil {
+			return nil
+		}
+		if terminatorCount == expectedTerminatorCount {
+			terminatorsPresent = true
+		} else {
+			time.Sleep(5 * time.Second)
+		}
+		if time.Since(lastLog) > time.Minute {
+			logger.Infof("current terminator count: %v, elapsed time: %v", terminatorCount, time.Since(start))
+			lastLog = time.Now()
+		}
+	}
+
+	if terminatorsPresent {
+		logger.Infof("all terminators present, elapsed time: %v", time.Since(start))
+	} else {
+		return fmt.Errorf("fail to reach expected terminator count of %v on controller %v", expectedTerminatorCount, c.Id)
+	}
+
+	for {
+		count, err := validateRouterSdkTerminators(c.Id, clients)
+		if err == nil {
+			return nil
+		}
+
+		if time.Now().After(deadline) {
+			return err
+		}
+
+		logger.Infof("current count of invalid sdk terminators: %v, elapsed time: %v", count, time.Since(start))
+		time.Sleep(15 * time.Second)
+	}
+}
+
+func getTerminatorCount(clients *zitirest.Clients) (int64, error) {
+	ctx, cancelF := context.WithTimeout(context.Background(), 15*time.Second)
+	defer cancelF()
+
+	filter := "limit 1"
+	result, err := clients.Fabric.Terminator.ListTerminators(&terminator.ListTerminatorsParams{
+		Filter:  &filter,
+		Context: ctx,
+	})
+
+	if err != nil {
+		return 0, err
+	}
+	count := *result.Payload.Meta.Pagination.TotalCount
+	return count, nil
+}
+
+func validateRouterSdkTerminators(id string, clients *zitirest.Clients) (int, error) {
+	logger := pfxlog.Logger().WithField("ctrl", id)
+
+	closeNotify := make(chan struct{})
+	eventNotify := make(chan *mgmt_pb.RouterSdkTerminatorsDetails, 1)
+
+	handleSdkTerminatorResults := func(msg *channel.Message, _ channel.Channel) {
+		detail := &mgmt_pb.RouterSdkTerminatorsDetails{}
+		if err := proto.Unmarshal(msg.Body, detail); err != nil {
+			pfxlog.Logger().WithError(err).Error("unable to unmarshal router sdk terminator details")
+			return
+		}
+		eventNotify <- detail
+	}
+
+	bindHandler := func(binding channel.Binding) error {
+		binding.AddReceiveHandlerF(int32(mgmt_pb.ContentType_ValidateRouterSdkTerminatorsResultType), handleSdkTerminatorResults)
+		binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) {
+			close(closeNotify)
+		}))
+		return nil
+	}
+
+	ch, err := clients.NewWsMgmtChannel(channel.BindHandlerF(bindHandler))
+	if err != nil {
+		return 0, err
+	}
+
+	defer func() {
+		_ = ch.Close()
+	}()
+
+	request := &mgmt_pb.ValidateRouterSdkTerminatorsRequest{
+		Filter: "limit none",
+	}
+	responseMsg, err := protobufs.MarshalTyped(request).WithTimeout(10 * time.Second).SendForReply(ch)
+
+	response := &mgmt_pb.ValidateRouterSdkTerminatorsResponse{}
+	if err = protobufs.TypedResponse(response).Unmarshall(responseMsg, err); err != nil {
+		return 0, err
+	}
+
+	if !response.Success {
+		return 0, fmt.Errorf("failed to start sdk terminator validation: %s", response.Message)
+	}
+
+	logger.Infof("started validation of %v routers", response.RouterCount)
+
+	expected := response.RouterCount
+
+	invalid := 0
+	for expected > 0 {
+		select {
+		case <-closeNotify:
+			fmt.Printf("channel closed, exiting")
+			return 0, errors.New("unexpected close of mgmt channel")
+		case routerDetail := <-eventNotify:
+			if !routerDetail.ValidateSuccess {
+				return invalid, fmt.Errorf("error: unable to validate on controller %s (%s)", routerDetail.Message, id)
+			}
+			for _, linkDetail := range routerDetail.Details {
+				if !linkDetail.IsValid {
+					invalid++
+				}
+			}
+			expected--
+		}
+	}
+	if invalid == 0 {
+		logger.Infof("sdk terminator validation of %v routers successful", response.RouterCount)
+		return invalid, nil
+	}
+	return invalid, fmt.Errorf("invalid sdk terminators found")
+}
diff --git a/zititest/zitilab/actions/edge/init_identities.go b/zititest/zitilab/actions/edge/init_identities.go
index 3eae38a20..47faa42bd 100644
--- a/zititest/zitilab/actions/edge/init_identities.go
+++ b/zititest/zitilab/actions/edge/init_identities.go
@@ -31,7 +31,7 @@ func (action *initIdentitiesAction) createAndEnrollIdentity(run model.Run, c *mo
 
 	jwtFileName := filepath.Join(run.GetTmpDir(), c.Id+".jwt")
 
-	err := zitilib_actions.EdgeExec(c.GetModel(), "create", "identity", "service", c.Id,
+	err := zitilib_actions.EdgeExec(c.GetModel(), "create", "identity", c.Id,
 		"--jwt-output-file", jwtFileName,
 		"-a", strings.Join(c.Tags, ","))
 
diff --git a/zititest/zitilab/cli/cli.go b/zititest/zitilab/cli/cli.go
index 6b463884d..dfd4b1e4f 100644
--- a/zititest/zitilab/cli/cli.go
+++ b/zititest/zitilab/cli/cli.go
@@ -44,3 +44,44 @@ func Exec(m *model.Model, args ...string) (string, error) {
 
 	return cliOut.String(), nil
 }
+
+func NewSeq() *Seq {
+	return &Seq{}
+}
+
+type Seq struct {
+	err error
+}
+
+func (self *Seq) Error() error {
+	return self.err
+}
+
+func (self *Seq) Args(args ...string) []string {
+	return args
+}
+
+func (self *Seq) Exec(args ...string) {
+	self.ExecF(args, nil)
+}
+
+func (self *Seq) ExecF(args []string, f func(string) error) {
+	if self.err != nil {
+		return
+	}
+
+	var cliOut bytes.Buffer
+	var cliErr bytes.Buffer
+
+	ziticli := cmd.NewRootCommand(os.Stdin, &cliOut, &cliErr)
+	ziticli.SetArgs(args)
+	logrus.Infof("executing: %s", strings.Join(args, " "))
+	if err := ziticli.Execute(); err != nil {
+		logrus.Errorf("err executing command, err:[%e]", err)
+		self.err = err
+	}
+
+	if self.err == nil && f != nil {
+		self.err = f(cliOut.String())
+	}
+}
diff --git a/zititest/zitilab/component_common.go b/zititest/zitilab/component_common.go
index 6ac064322..31d23922a 100644
--- a/zititest/zitilab/component_common.go
+++ b/zititest/zitilab/component_common.go
@@ -27,10 +27,11 @@ import (
 
 func getZitiProcessFilter(c *model.Component, zitiType string) func(string) bool {
 	return func(s string) bool {
-		return strings.Contains(s, "ziti") &&
+		matches := strings.Contains(s, "ziti") &&
 			strings.Contains(s, zitiType) &&
 			strings.Contains(s, fmt.Sprintf("--cli-agent-alias %s ", c.Id)) &&
 			!strings.Contains(s, "sudo ")
+		return matches
 	}
 }
 
diff --git a/zititest/zitilab/component_ziti_tunnel.go b/zititest/zitilab/component_ziti_tunnel.go
index daca6af97..9ddab77ac 100644
--- a/zititest/zitilab/component_ziti_tunnel.go
+++ b/zititest/zitilab/component_ziti_tunnel.go
@@ -80,11 +80,18 @@ func (self *ZitiTunnelType) StageFiles(r model.Run, c *model.Component) error {
 
 func (self *ZitiTunnelType) InitializeHost(_ model.Run, c *model.Component) error {
 	if self.Mode == ZitiTunnelModeTproxy {
-		cmds := []string{
-			"sudo sed -i 's/#DNS=/DNS=127.0.0.1/g' /etc/systemd/resolved.conf",
-			"sudo systemctl restart systemd-resolved",
+		key := "ziti_tunnel.resolve_setup_done"
+		if _, found := c.Host.Data[key]; !found {
+			cmds := []string{
+				"sudo sed -i 's/#DNS=/DNS=127.0.0.1/g' /etc/systemd/resolved.conf",
+				"sudo systemctl restart systemd-resolved",
+			}
+			if err := c.Host.ExecLogOnlyOnError(cmds...); err != nil {
+				return err
+			}
+			c.Host.Data[key] = true
+			return nil
 		}
-		return c.Host.ExecLogOnlyOnError(cmds...)
 	}
 	return nil
 }
@@ -122,8 +129,8 @@ func (self *ZitiTunnelType) Start(_ model.Run, c *model.Component) error {
 		useSudo = "sudo"
 	}
 
-	serviceCmd := fmt.Sprintf("%s %s tunnel %s -v --log-formatter pfxlog -i %s --cli-agent-alias %s > %s 2>&1 &",
-		useSudo, binaryPath, mode.String(), configPath, c.Id, logsPath)
+	serviceCmd := fmt.Sprintf("%s %s tunnel %s -v --cli-agent-alias %s --log-formatter pfxlog -i %s > %s 2>&1 &",
+		useSudo, binaryPath, mode.String(), c.Id, configPath, logsPath)
 
 	value, err := c.Host.ExecLogged(
 		"rm -f "+logsPath,
diff --git a/zititest/zitilab/component_zrok_looptest.go b/zititest/zitilab/component_zrok_looptest.go
index 6b529ee5e..26c6b971e 100644
--- a/zititest/zitilab/component_zrok_looptest.go
+++ b/zititest/zitilab/component_zrok_looptest.go
@@ -86,7 +86,7 @@ func (self *ZrokLoopTestType) Start(_ model.Run, c *model.Component) error {
 	logsPath := fmt.Sprintf("/home/%s/logs/%s.log", user, c.Id)
 
 	maxDwell := 1000 + c.ScaleIndex
-	serviceCmd := fmt.Sprintf("nohup sudo -u %s %s test loop public --iterations %v --loopers %v --min-pacing-ms %v --max-pacing-ms %v "+
+	serviceCmd := fmt.Sprintf("nohup sudo -u %s %s test loop public -v --iterations %v --loopers %v --min-pacing-ms %v --max-pacing-ms %v "+
 		"--max-dwell-ms %d 2>&1 &> %s &",
 		userId, binaryPath, self.Iterations, self.Loopers, self.Pacing.Milliseconds(), self.Pacing.Milliseconds(), maxDwell, logsPath)
 
diff --git a/zititest/zitilab/models/db_builder.go b/zititest/zitilab/models/db_builder.go
index e05c15aff..f6938e6e9 100644
--- a/zititest/zitilab/models/db_builder.go
+++ b/zititest/zitilab/models/db_builder.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"github.com/openziti/fablab/kernel/model"
 	"github.com/openziti/storage/boltz"
+	"github.com/openziti/ziti/controller/command"
 	"github.com/openziti/ziti/controller/db"
 	"github.com/openziti/ziti/controller/network"
 	"github.com/openziti/ziti/zititest/zitilab"
@@ -52,7 +53,7 @@ func (self *ZitiDbBuilder) Build(m *model.Model) error {
 		}
 	}()
 
-	self.stores, err = db.InitStores(self.zitiDb)
+	self.stores, err = db.InitStores(self.zitiDb, command.NoOpRateLimiter{})
 	if err != nil {
 		return errors.Wrapf(err, "unable to init fabric stores using db [%v]", dbFile)
 	}

From 4f7502b888f6075cd110fd22c4fb338da4721c4a Mon Sep 17 00:00:00 2001
From: Paul Lorenz <paul.lorenz@netfoundry.io>
Date: Tue, 12 Mar 2024 16:36:22 -0400
Subject: [PATCH 44/46] Fix panic on second api session sync failed notify in
 router. Fixes #1815

---
 router/handler_edge_ctrl/apiSessionAdded.go | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/router/handler_edge_ctrl/apiSessionAdded.go b/router/handler_edge_ctrl/apiSessionAdded.go
index 54866c2d6..a71525c82 100644
--- a/router/handler_edge_ctrl/apiSessionAdded.go
+++ b/router/handler_edge_ctrl/apiSessionAdded.go
@@ -129,18 +129,21 @@ func (h *apiSessionAddedHandler) syncFailed(err error) {
 	h.trackerLock.Lock()
 	defer h.trackerLock.Unlock()
 
-	logrus.WithError(err).Error("failed to synchronize api sessions")
+	// can be called twice, only notify the first time
+	if h.syncTracker != nil {
+		logrus.WithError(err).Error("failed to synchronize api sessions")
 
-	h.syncTracker.Stop()
-	h.sm.MarkSyncStopped(h.syncTracker.syncId)
+		h.syncTracker.Stop()
+		h.sm.MarkSyncStopped(h.syncTracker.syncId)
 
-	h.syncTracker = nil
+		h.syncTracker = nil
 
-	resync := &edge_ctrl_pb.RequestClientReSync{
-		Reason: fmt.Sprintf("error during api session sync: %v", err),
-	}
-	if err := protobufs.MarshalTyped(resync).Send(h.control); err != nil {
-		logrus.WithError(err).Error("failed to send request client re-sync message")
+		resync := &edge_ctrl_pb.RequestClientReSync{
+			Reason: fmt.Sprintf("error during api session sync: %v", err),
+		}
+		if err := protobufs.MarshalTyped(resync).Send(h.control); err != nil {
+			logrus.WithError(err).Error("failed to send request client re-sync message")
+		}
 	}
 }
 

From 33a4479793ecc4740c142aa5a061e8f1cfcb15f9 Mon Sep 17 00:00:00 2001
From: Paul Lorenz <paul.lorenz@netfoundry.io>
Date: Tue, 12 Mar 2024 16:41:15 -0400
Subject: [PATCH 45/46] Update changelog and deps

---
 CHANGELOG.md    |  2 ++
 go.mod          | 10 +++++-----
 go.sum          | 11 +++++++++++
 zititest/go.mod | 10 +++++-----
 zititest/go.sum | 24 ++++++++++++------------
 5 files changed, 35 insertions(+), 22 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9080be82f..1493a1919 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,8 +33,10 @@ back to the routers when it's got too much work.
 
 * github.com/openziti/edge-api: [v0.26.10 -> v0.26.12](https://github.com/openziti/edge-api/compare/v0.26.10...v0.26.12)
 * github.com/openziti/ziti: [v0.32.2 -> v0.33.0](https://github.com/openziti/ziti/compare/v0.32.2...v0.33.0)
+    * [Issue #1815](https://github.com/openziti/ziti/issues/1815) - Panic if api session sync failed handler is called twice in the router
     * [Issue #1794](https://github.com/openziti/ziti/issues/1794) - Add SDK terminator chaos test and fix any bugs found as part of chaos testing
     * [Issue #1369](https://github.com/openziti/ziti/issues/1369) - Allow filtering by policy type when listing identities for service or services for identity
+    * [Issue #1791](https://github.com/openziti/ziti/issues/1791) - route dial isn't checking for network timeouts correctly
     * [Issue #1204](https://github.com/openziti/ziti/issues/1204) - ziti cli identity tags related flags misbehaving
     * [Issue #987](https://github.com/openziti/ziti/issues/987) - "ziti create config router edge" doesn't know about --tunnelerMode proxy
     * [Issue #652](https://github.com/openziti/ziti/issues/652) - Update CLI script M1 Support when github actions allows
diff --git a/go.mod b/go.mod
index 4586eedd4..ab52c9f13 100644
--- a/go.mod
+++ b/go.mod
@@ -18,11 +18,11 @@ require (
 	github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
 	github.com/gaissmai/extnetip v0.4.0
 	github.com/go-acme/lego/v4 v4.15.0
-	github.com/go-openapi/errors v0.21.0
+	github.com/go-openapi/errors v0.22.0
 	github.com/go-openapi/loads v0.21.5
 	github.com/go-openapi/runtime v0.27.1
 	github.com/go-openapi/spec v0.20.14
-	github.com/go-openapi/strfmt v0.22.1
+	github.com/go-openapi/strfmt v0.23.0
 	github.com/go-openapi/swag v0.22.9
 	github.com/go-openapi/validate v0.23.0
 	github.com/go-resty/resty/v2 v2.11.0
@@ -67,13 +67,13 @@ require (
 	github.com/rabbitmq/amqp091-go v1.8.1
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
 	github.com/russross/blackfriday v1.6.0
-	github.com/shirou/gopsutil/v3 v3.24.1
+	github.com/shirou/gopsutil/v3 v3.24.2
 	github.com/sirupsen/logrus v1.9.3
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/spf13/cobra v1.8.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.18.2
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125
 	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/zitadel/oidc/v2 v2.12.0
@@ -173,7 +173,7 @@ require (
 	github.com/valyala/fasttemplate v1.2.2 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
-	github.com/yusufpapurcu/wmi v1.2.3 // indirect
+	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
 	go.opentelemetry.io/otel v1.24.0 // indirect
diff --git a/go.sum b/go.sum
index 330007943..9a936a3d5 100644
--- a/go.sum
+++ b/go.sum
@@ -222,6 +222,8 @@ github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE
 github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo=
 github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY=
 github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
+github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
+github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
 github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
 github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
 github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
@@ -234,6 +236,8 @@ github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/
 github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
 github.com/go-openapi/strfmt v0.22.1 h1:5Ky8cybT4576C6Ffc+8gYji/wRXCo6Ozm8RaWjPI6jc=
 github.com/go-openapi/strfmt v0.22.1/go.mod h1:OfVoytIXJasDkkGvkb1Cceb3BPyMOwk1FgmyyEw7NYg=
+github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
+github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
 github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
 github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
 github.com/go-openapi/validate v0.23.0 h1:2l7PJLzCis4YUGEoW6eoQw3WhyM65WSIcjX6SQnlfDw=
@@ -674,6 +678,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
 github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
+github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
+github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
 github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
 github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -747,6 +753,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
 github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -759,6 +766,8 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -794,6 +803,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
 github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4=
 github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
diff --git a/zititest/go.mod b/zititest/go.mod
index 377636e53..b35f417cb 100644
--- a/zititest/go.mod
+++ b/zititest/go.mod
@@ -26,7 +26,7 @@ require (
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.8.0
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.9.0
 	go.etcd.io/bbolt v1.3.9
 	golang.org/x/net v0.21.0
 	google.golang.org/protobuf v1.32.0
@@ -71,12 +71,12 @@ require (
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
 	github.com/go-openapi/analysis v0.22.2 // indirect
-	github.com/go-openapi/errors v0.21.0 // indirect
+	github.com/go-openapi/errors v0.22.0 // indirect
 	github.com/go-openapi/jsonpointer v0.20.2 // indirect
 	github.com/go-openapi/jsonreference v0.20.4 // indirect
 	github.com/go-openapi/loads v0.21.5 // indirect
 	github.com/go-openapi/spec v0.20.14 // indirect
-	github.com/go-openapi/strfmt v0.22.1 // indirect
+	github.com/go-openapi/strfmt v0.23.0 // indirect
 	github.com/go-openapi/swag v0.22.9 // indirect
 	github.com/go-openapi/validate v0.23.0 // indirect
 	github.com/go-resty/resty/v2 v2.11.0 // indirect
@@ -157,7 +157,7 @@ require (
 	github.com/russross/blackfriday v1.6.0 // indirect
 	github.com/sagikazarmark/locafero v0.4.0 // indirect
 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
-	github.com/shirou/gopsutil/v3 v3.24.1 // indirect
+	github.com/shirou/gopsutil/v3 v3.24.2 // indirect
 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
@@ -175,7 +175,7 @@ require (
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
-	github.com/yusufpapurcu/wmi v1.2.3 // indirect
+	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 	github.com/zitadel/oidc/v2 v2.12.0 // indirect
 	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
diff --git a/zititest/go.sum b/zititest/go.sum
index 3a3f66eb1..dddafba1a 100644
--- a/zititest/go.sum
+++ b/zititest/go.sum
@@ -225,8 +225,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
 github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0=
 github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo=
-github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY=
-github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
+github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
+github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
 github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
 github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
 github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
@@ -237,8 +237,8 @@ github.com/go-openapi/runtime v0.27.1 h1:ae53yaOoh+fx/X5Eaq8cRmavHgDma65XPZuvBqv
 github.com/go-openapi/runtime v0.27.1/go.mod h1:fijeJEiEclyS8BRurYE1DE5TLb9/KZl6eAdbzjsrlLU=
 github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
 github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
-github.com/go-openapi/strfmt v0.22.1 h1:5Ky8cybT4576C6Ffc+8gYji/wRXCo6Ozm8RaWjPI6jc=
-github.com/go-openapi/strfmt v0.22.1/go.mod h1:OfVoytIXJasDkkGvkb1Cceb3BPyMOwk1FgmyyEw7NYg=
+github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
+github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
 github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
 github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
 github.com/go-openapi/validate v0.23.0 h1:2l7PJLzCis4YUGEoW6eoQw3WhyM65WSIcjX6SQnlfDw=
@@ -698,8 +698,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
 github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
-github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
-github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
+github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
+github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
 github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
 github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -772,8 +772,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
-github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -784,8 +784,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -819,8 +820,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
-github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4=
 github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -1098,7 +1099,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

From ef42a86c759fe0f16d75f33aa2c8846b335f162b Mon Sep 17 00:00:00 2001
From: Shawn Carey <shawn.carey@netfoundry.io>
Date: Tue, 12 Mar 2024 21:29:07 +0000
Subject: [PATCH 46/46] changelog entry for 1781 (#1817)

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1493a1919..0b9231aa9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,7 @@ back to the routers when it's got too much work.
 * github.com/openziti/ziti: [v0.32.2 -> v0.33.0](https://github.com/openziti/ziti/compare/v0.32.2...v0.33.0)
     * [Issue #1815](https://github.com/openziti/ziti/issues/1815) - Panic if api session sync failed handler is called twice in the router
     * [Issue #1794](https://github.com/openziti/ziti/issues/1794) - Add SDK terminator chaos test and fix any bugs found as part of chaos testing
+    * [Issue #1781](https://github.com/openziti/ziti/issues/1781) - Improve performance when adding intercepted services
     * [Issue #1369](https://github.com/openziti/ziti/issues/1369) - Allow filtering by policy type when listing identities for service or services for identity
     * [Issue #1791](https://github.com/openziti/ziti/issues/1791) - route dial isn't checking for network timeouts correctly
     * [Issue #1204](https://github.com/openziti/ziti/issues/1204) - ziti cli identity tags related flags misbehaving