mirror of
https://github.com/openziti/ziti.git
synced 2026-10-12 07:18:36 +00:00
867 lines
25 KiB
Go
867 lines
25 KiB
Go
/*
|
|
(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 router
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/openziti/foundation/v2/rate"
|
|
"github.com/openziti/ziti/common"
|
|
"github.com/openziti/ziti/controller/command"
|
|
"github.com/openziti/ziti/router/state"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"plugin"
|
|
"runtime/debug"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/openziti/foundation/v2/debugz"
|
|
"github.com/openziti/foundation/v2/goroutines"
|
|
"github.com/openziti/xweb/v2"
|
|
"github.com/openziti/ziti/common/config"
|
|
"github.com/openziti/ziti/router/link"
|
|
metrics2 "github.com/rcrowley/go-metrics"
|
|
|
|
gosundheit "github.com/AppsFlyer/go-sundheit"
|
|
"github.com/AppsFlyer/go-sundheit/checks"
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/channel/v2"
|
|
"github.com/openziti/foundation/v2/concurrenz"
|
|
"github.com/openziti/foundation/v2/errorz"
|
|
"github.com/openziti/foundation/v2/versions"
|
|
"github.com/openziti/identity"
|
|
"github.com/openziti/metrics"
|
|
"github.com/openziti/transport/v2"
|
|
"github.com/openziti/ziti/common/health"
|
|
fabricMetrics "github.com/openziti/ziti/common/metrics"
|
|
"github.com/openziti/ziti/common/pb/ctrl_pb"
|
|
"github.com/openziti/ziti/common/profiler"
|
|
"github.com/openziti/ziti/router/env"
|
|
"github.com/openziti/ziti/router/forwarder"
|
|
"github.com/openziti/ziti/router/handler_ctrl"
|
|
"github.com/openziti/ziti/router/handler_link"
|
|
"github.com/openziti/ziti/router/handler_xgress"
|
|
"github.com/openziti/ziti/router/xgress"
|
|
"github.com/openziti/ziti/router/xgress_proxy"
|
|
"github.com/openziti/ziti/router/xgress_proxy_udp"
|
|
"github.com/openziti/ziti/router/xgress_transport"
|
|
"github.com/openziti/ziti/router/xgress_transport_udp"
|
|
"github.com/openziti/ziti/router/xlink"
|
|
"github.com/openziti/ziti/router/xlink_transport"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"google.golang.org/protobuf/proto"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Router struct {
|
|
config *Config
|
|
ctrls env.NetworkControllers
|
|
ctrlBindhandler channel.BindHandler
|
|
faulter *forwarder.Faulter
|
|
forwarder *forwarder.Forwarder
|
|
xrctrls []env.Xrctrl
|
|
xlinkFactories map[string]xlink.Factory
|
|
xlinkListeners []xlink.Listener
|
|
xlinkDialers []xlink.Dialer
|
|
xlinkRegistry xlink.Registry
|
|
xgressListeners []xgress.Listener
|
|
linkDialerPool goroutines.Pool
|
|
rateLimiterPool goroutines.Pool
|
|
ctrlRateLimiter rate.AdaptiveRateLimitTracker
|
|
metricsRegistry metrics.UsageRegistry
|
|
shutdownC chan struct{}
|
|
shutdownDoneC chan struct{}
|
|
isShutdown atomic.Bool
|
|
metricsReporter metrics.Handler
|
|
versionProvider versions.VersionProvider
|
|
debugOperations map[byte]func(c *bufio.ReadWriter) error
|
|
stateManager state.Manager
|
|
|
|
xwebs []xweb.Instance
|
|
xwebFactoryRegistry xweb.Registry
|
|
agentBindHandlers []channel.BindHandler
|
|
}
|
|
|
|
func (self *Router) GetRouterId() *identity.TokenId {
|
|
return self.config.Id
|
|
}
|
|
|
|
func (self *Router) GetNetworkControllers() env.NetworkControllers {
|
|
return self.ctrls
|
|
}
|
|
|
|
func (self *Router) GetDialerCfg() map[string]xgress.OptionsData {
|
|
return self.config.Dialers
|
|
}
|
|
|
|
func (self *Router) GetXlinkDialers() []xlink.Dialer {
|
|
return self.xlinkDialers
|
|
}
|
|
|
|
func (self *Router) GetXrctrls() []env.Xrctrl {
|
|
return self.xrctrls
|
|
}
|
|
|
|
func (self *Router) GetTraceHandler() *channel.TraceHandler {
|
|
return self.config.Trace.Handler
|
|
}
|
|
|
|
func (self *Router) GetXlinkRegistry() xlink.Registry {
|
|
return self.xlinkRegistry
|
|
}
|
|
|
|
func (self *Router) GetCloseNotify() <-chan struct{} {
|
|
return self.shutdownC
|
|
}
|
|
|
|
func (self *Router) GetMetricsRegistry() metrics.UsageRegistry {
|
|
return self.metricsRegistry
|
|
}
|
|
|
|
func (self *Router) RenderJsonConfig() (string, error) {
|
|
jsonMap, err := config.ToJsonCompatibleMap(self.config.src)
|
|
delete(jsonMap, FlagsCfgMapKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
b, err := json.Marshal(jsonMap)
|
|
return string(b), err
|
|
}
|
|
|
|
func (self *Router) GetChannel(controllerId string) channel.Channel {
|
|
return self.ctrls.GetCtrlChannel(controllerId)
|
|
}
|
|
|
|
func (self *Router) DefaultRequestTimeout() time.Duration {
|
|
return self.config.Ctrl.DefaultRequestTimeout
|
|
}
|
|
|
|
func (self *Router) GetHeartbeatOptions() env.HeartbeatOptions {
|
|
return self.config.Ctrl.Heartbeats
|
|
}
|
|
|
|
func (self *Router) GetStateManager() state.Manager {
|
|
return self.stateManager
|
|
}
|
|
|
|
func (self *Router) GetRouterDataModel() *common.RouterDataModel {
|
|
return self.stateManager.RouterDataModel()
|
|
}
|
|
|
|
func (self *Router) IsHaEnabled() bool {
|
|
return self.config.Ha.Enabled
|
|
}
|
|
|
|
func Create(config *Config, versionProvider versions.VersionProvider) *Router {
|
|
closeNotify := make(chan struct{})
|
|
|
|
if config.Metrics.IntervalAgeThreshold != 0 {
|
|
metrics.SetIntervalAgeThreshold(config.Metrics.IntervalAgeThreshold)
|
|
logrus.Infof("set interval age threshold to '%v'", config.Metrics.IntervalAgeThreshold)
|
|
}
|
|
env.IntervalSize = config.Metrics.ReportInterval
|
|
metricsRegistry := metrics.NewUsageRegistry(config.Id.Token, map[string]string{}, closeNotify)
|
|
xgress.InitMetrics(metricsRegistry)
|
|
|
|
linkDialerPoolConfig := goroutines.PoolConfig{
|
|
QueueSize: uint32(config.Forwarder.LinkDial.QueueLength),
|
|
MinWorkers: 0,
|
|
MaxWorkers: uint32(config.Forwarder.LinkDial.WorkerCount),
|
|
IdleTime: 30 * time.Second,
|
|
CloseNotify: closeNotify,
|
|
PanicHandler: func(err interface{}) {
|
|
pfxlog.Logger().WithField(logrus.ErrorKey, err).WithField("backtrace", string(debug.Stack())).Error("panic during link dial")
|
|
},
|
|
}
|
|
|
|
fabricMetrics.ConfigureGoroutinesPoolMetrics(&linkDialerPoolConfig, metricsRegistry, "pool.link.dialer")
|
|
|
|
linkDialerPool, err := goroutines.NewPool(linkDialerPoolConfig)
|
|
if err != nil {
|
|
panic(errors.Wrap(err, "error creating link dialer pool"))
|
|
}
|
|
|
|
router := &Router{
|
|
config: config,
|
|
metricsRegistry: metricsRegistry,
|
|
shutdownC: closeNotify,
|
|
shutdownDoneC: make(chan struct{}),
|
|
versionProvider: versionProvider,
|
|
debugOperations: map[byte]func(c *bufio.ReadWriter) error{},
|
|
xwebFactoryRegistry: xweb.NewRegistryMap(),
|
|
linkDialerPool: linkDialerPool,
|
|
ctrlRateLimiter: command.NewAdaptiveRateLimitTracker(config.Ctrl.RateLimit, metricsRegistry, closeNotify),
|
|
}
|
|
|
|
router.stateManager = state.NewManager(router)
|
|
|
|
router.ctrls = env.NewNetworkControllers(config.Ctrl.DefaultRequestTimeout, router.connectToController, &config.Ctrl.Heartbeats)
|
|
router.xlinkRegistry = link.NewLinkRegistry(router)
|
|
router.faulter = forwarder.NewFaulter(router.ctrls, config.Forwarder.FaultTxInterval, closeNotify)
|
|
router.forwarder = forwarder.NewForwarder(metricsRegistry, router.faulter, config.Forwarder, closeNotify)
|
|
router.forwarder.StartScanner(router.ctrls)
|
|
|
|
xgress.InitPayloadIngester(closeNotify)
|
|
xgress.InitAcker(router.forwarder, metricsRegistry, closeNotify)
|
|
xgress.InitRetransmitter(router.forwarder, router.forwarder, metricsRegistry, closeNotify)
|
|
|
|
router.ctrlBindhandler, err = handler_ctrl.NewBindHandler(router, router.forwarder, router)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return router
|
|
}
|
|
|
|
func (self *Router) RegisterXrctrl(x env.Xrctrl) error {
|
|
if err := self.config.Configure(x); err != nil {
|
|
return err
|
|
}
|
|
if x.Enabled() {
|
|
self.xrctrls = append(self.xrctrls, x)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) GetVersionInfo() versions.VersionProvider {
|
|
return self.versionProvider
|
|
}
|
|
|
|
func (self *Router) GetConfig() *Config {
|
|
return self.config
|
|
}
|
|
|
|
func (self *Router) Start() error {
|
|
if err := os.MkdirAll(self.config.Ctrl.DataDir, 0700); err != nil {
|
|
logrus.WithField("dir", self.config.Ctrl.DataDir).WithError(err).Error("failed to initialize data directory")
|
|
return err
|
|
}
|
|
|
|
self.showOptions()
|
|
if err := self.initRateLimiterPool(); err != nil {
|
|
return err
|
|
}
|
|
self.startProfiling()
|
|
|
|
healthChecker, err := self.initializeHealthChecks()
|
|
if err != nil {
|
|
logrus.WithError(err).Fatalf("failed to create health checker")
|
|
}
|
|
|
|
if err := self.RegisterXWebHandlerFactory(health.NewHealthCheckApiFactory(healthChecker)); err != nil {
|
|
logrus.WithError(err).Fatalf("failed to create health checks api factory")
|
|
}
|
|
|
|
if err := self.registerComponents(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := self.registerPlugins(); err != nil {
|
|
return err
|
|
}
|
|
|
|
self.startXlinkDialers()
|
|
self.startXlinkListeners()
|
|
self.setDefaultDialerBindings()
|
|
self.startXgressListeners()
|
|
|
|
for _, web := range self.xwebs {
|
|
go web.Run()
|
|
}
|
|
|
|
if err = self.startControlPlane(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) Shutdown() error {
|
|
var errs []error
|
|
if self.isShutdown.CompareAndSwap(false, true) {
|
|
if err := self.ctrls.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
close(self.shutdownC)
|
|
|
|
for _, xlinkListener := range self.xlinkListeners {
|
|
if err := xlinkListener.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
self.xlinkRegistry.Shutdown()
|
|
|
|
for _, xgressListener := range self.xgressListeners {
|
|
if err := xgressListener.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
for _, web := range self.xwebs {
|
|
go web.Shutdown()
|
|
}
|
|
|
|
close(self.shutdownDoneC)
|
|
}
|
|
if len(errs) == 0 {
|
|
return nil
|
|
}
|
|
if len(errs) == 1 {
|
|
return errs[0]
|
|
}
|
|
return errorz.MultipleErrors(errs)
|
|
}
|
|
|
|
func (self *Router) Run() error {
|
|
if err := self.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
<-self.shutdownDoneC
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) showOptions() {
|
|
if output, err := json.Marshal(self.config.Ctrl.Options); err == nil {
|
|
pfxlog.Logger().Infof("ctrl = %s", string(output))
|
|
} else {
|
|
logrus.Fatalf("unable to display options (%v)", err)
|
|
}
|
|
|
|
if output, err := json.Marshal(self.config.Metrics); err == nil {
|
|
pfxlog.Logger().Infof("metrics = %s", string(output))
|
|
} else {
|
|
logrus.Fatalf("unable to display options (%v)", err)
|
|
}
|
|
}
|
|
|
|
func (self *Router) startProfiling() {
|
|
if self.config.Profile.Memory.Path != "" {
|
|
go profiler.NewMemoryWithShutdown(self.config.Profile.Memory.Path, self.config.Profile.Memory.Interval, self.shutdownC).Run()
|
|
}
|
|
if self.config.Profile.CPU.Path != "" {
|
|
if cpu, err := profiler.NewCPUWithShutdown(self.config.Profile.CPU.Path, self.shutdownC); err == nil {
|
|
go cpu.Run()
|
|
} else {
|
|
logrus.Errorf("unexpected error launching cpu profiling (%v)", err)
|
|
}
|
|
}
|
|
go newRouterMonitor(self.forwarder, self.shutdownC).Monitor()
|
|
}
|
|
|
|
func (self *Router) initRateLimiterPool() error {
|
|
rateLimiterPoolConfig := goroutines.PoolConfig{
|
|
QueueSize: uint32(self.forwarder.Options.RateLimiter.QueueLength),
|
|
MinWorkers: 0,
|
|
MaxWorkers: uint32(self.forwarder.Options.RateLimiter.WorkerCount),
|
|
IdleTime: 30 * time.Second,
|
|
CloseNotify: self.GetCloseNotify(),
|
|
PanicHandler: func(err interface{}) {
|
|
pfxlog.Logger().WithField(logrus.ErrorKey, err).WithField("backtrace", string(debug.Stack())).Error("panic during rate limited operation")
|
|
},
|
|
}
|
|
|
|
fabricMetrics.ConfigureGoroutinesPoolMetrics(&rateLimiterPoolConfig, self.GetMetricsRegistry(), "pool.rate_limiter")
|
|
|
|
rateLimiterPool, err := goroutines.NewPool(rateLimiterPoolConfig)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error creating rate limited pool")
|
|
}
|
|
|
|
self.rateLimiterPool = rateLimiterPool
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) GetLinkDialerPool() goroutines.Pool {
|
|
return self.linkDialerPool
|
|
}
|
|
|
|
func (self *Router) GetRateLimiterPool() goroutines.Pool {
|
|
return self.rateLimiterPool
|
|
}
|
|
|
|
func (self *Router) GetCtrlRateLimiter() rate.AdaptiveRateLimitTracker {
|
|
return self.ctrlRateLimiter
|
|
}
|
|
|
|
func (self *Router) registerComponents() error {
|
|
self.xlinkFactories = make(map[string]xlink.Factory)
|
|
xlinkAccepter := newXlinkAccepter(self.forwarder)
|
|
xlinkChAccepter := handler_link.NewBindHandlerFactory(
|
|
self.ctrls,
|
|
self.forwarder,
|
|
&self.config.Link.Heartbeats,
|
|
self.metricsRegistry,
|
|
self.xlinkRegistry,
|
|
)
|
|
|
|
linkTransportConfig := map[interface{}]interface{}{}
|
|
for k, v := range self.config.Transport {
|
|
linkTransportConfig[k] = v
|
|
}
|
|
linkTransportConfig[transport.KeyCachedProxyConfiguration] = self.config.Proxy
|
|
|
|
self.xlinkFactories["transport"] = xlink_transport.NewFactory(xlinkAccepter, xlinkChAccepter, linkTransportConfig, self.xlinkRegistry, self.metricsRegistry)
|
|
|
|
xgress.GlobalRegistry().Register("proxy", xgress_proxy.NewFactory(self.config.Id, self.ctrls, self.config.Transport))
|
|
xgress.GlobalRegistry().Register("proxy_udp", xgress_proxy_udp.NewFactory(self.ctrls))
|
|
xgress.GlobalRegistry().Register("transport", xgress_transport.NewFactory(self.config.Id, self.ctrls, self.config.Transport))
|
|
xgress.GlobalRegistry().Register("transport_udp", xgress_transport_udp.NewFactory(self.config.Id, self.ctrls))
|
|
|
|
if err := self.RegisterXweb(xweb.NewDefaultInstance(self.xwebFactoryRegistry, self.config.Id)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if v, ok := self.xlinkRegistry.(env.Xrctrl); ok {
|
|
if err := self.RegisterXrctrl(v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) registerPlugins() error {
|
|
for _, pluginPath := range self.config.Plugins {
|
|
goPlugin, err := plugin.Open(pluginPath)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "router unable to load plugin at path %v", pluginPath)
|
|
}
|
|
initializeSymbol, err := goPlugin.Lookup("Initialize")
|
|
if err != nil {
|
|
return errors.Wrapf(err, "router plugin at %v does not contain Initialize symbol", pluginPath)
|
|
}
|
|
initialize, ok := initializeSymbol.(func(*Router) error)
|
|
if !ok {
|
|
return errors.Errorf("router plugin at %v exports Initialize symbol, but it is not of type 'func(router *router.Router) error'", pluginPath)
|
|
}
|
|
if err := initialize(self); err != nil {
|
|
return errors.Wrapf(err, "error initializing router plugin at %v", pluginPath)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) startXlinkDialers() {
|
|
for _, lmap := range self.config.Link.Dialers {
|
|
binding := "transport"
|
|
if bindingVal, ok := lmap["binding"]; ok {
|
|
bindingName := fmt.Sprintf("%v", bindingVal)
|
|
if len(bindingName) > 0 {
|
|
binding = bindingName
|
|
}
|
|
}
|
|
|
|
if factory, found := self.xlinkFactories[binding]; found {
|
|
dialer, err := factory.CreateDialer(self.config.Id, self.forwarder, lmap)
|
|
if err != nil {
|
|
logrus.Fatalf("error creating Xlink dialer (%v)", err)
|
|
}
|
|
self.xlinkDialers = append(self.xlinkDialers, dialer)
|
|
logrus.Infof("started Xlink dialer with binding [%s]", binding)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *Router) startXlinkListeners() {
|
|
for _, lmap := range self.config.Link.Listeners {
|
|
binding := "transport"
|
|
if bindingVal, ok := lmap["binding"]; ok {
|
|
bindingName := fmt.Sprintf("%v", bindingVal)
|
|
if len(bindingName) > 0 {
|
|
binding = bindingName
|
|
}
|
|
}
|
|
|
|
if factory, found := self.xlinkFactories[binding]; found {
|
|
lmap[transport.KeyProtocol] = "ziti-link"
|
|
listener, err := factory.CreateListener(self.config.Id, self.forwarder, lmap)
|
|
if err != nil {
|
|
logrus.Fatalf("error creating Xlink listener (%v)", err)
|
|
}
|
|
if err := listener.Listen(); err != nil {
|
|
logrus.Fatalf("error listening on Xlink (%v)", err)
|
|
}
|
|
self.xlinkListeners = append(self.xlinkListeners, listener)
|
|
logrus.Infof("started Xlink listener with binding [%s] advertising [%s]", binding, listener.GetAdvertisement())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *Router) setDefaultDialerBindings() {
|
|
if len(self.xlinkDialers) == 1 && len(self.xlinkListeners) == 1 && self.xlinkDialers[0].GetBinding() == "" {
|
|
self.xlinkDialers[0].AdoptBinding(self.xlinkListeners[0])
|
|
}
|
|
}
|
|
|
|
func (self *Router) startXgressListeners() {
|
|
for _, binding := range self.config.Listeners {
|
|
factory, err := xgress.GlobalRegistry().Factory(binding.name)
|
|
if err != nil {
|
|
logrus.Fatalf("error getting xgress factory [%s] (%v)", binding.name, err)
|
|
}
|
|
listener, err := factory.CreateListener(binding.options)
|
|
if err != nil {
|
|
logrus.Fatalf("error creating xgress listener [%s] (%v)", binding.name, err)
|
|
}
|
|
self.xgressListeners = append(self.xgressListeners, listener)
|
|
|
|
var address string
|
|
if addressVal, found := binding.options["address"]; found {
|
|
address = addressVal.(string)
|
|
}
|
|
|
|
err = listener.Listen(address,
|
|
handler_xgress.NewBindHandler(
|
|
handler_xgress.NewReceiveHandler(self.forwarder),
|
|
handler_xgress.NewCloseHandler(self.ctrls, self.forwarder),
|
|
self.forwarder,
|
|
),
|
|
)
|
|
if err != nil {
|
|
logrus.Fatalf("error listening [%s] (%v)", binding.name, err)
|
|
}
|
|
logrus.Infof("created xgress listener [%s] at [%s]", binding.name, address)
|
|
}
|
|
}
|
|
|
|
func (self *Router) startControlPlane() error {
|
|
endpoints, err := self.getInitialCtrlEndpoints()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log := pfxlog.Logger()
|
|
log.Infof("router configured with %v controller endpoints", len(endpoints))
|
|
|
|
self.ctrls.UpdateControllerEndpoints(endpoints)
|
|
|
|
self.metricsReporter = fabricMetrics.NewControllersReporter(self.ctrls)
|
|
self.metricsRegistry.StartReporting(self.metricsReporter, self.config.Metrics.ReportInterval, self.config.Metrics.MessageQueueSize)
|
|
|
|
if self.config.Ctrl.StartupTimeout > 0 {
|
|
time.AfterFunc(self.config.Ctrl.StartupTimeout, func() {
|
|
if !self.isShutdown.Load() && len(self.ctrls.GetAll()) == 0 {
|
|
if os.Getenv("STACKDUMP_ON_FAILED_STARTUP") == "true" {
|
|
debugz.DumpStack()
|
|
}
|
|
pfxlog.Logger().Fatal("unable to connect to any controllers before timeout")
|
|
}
|
|
})
|
|
}
|
|
|
|
_ = self.ctrls.AnyValidCtrlChannel()
|
|
for _, x := range self.xrctrls {
|
|
if err := x.Run(self); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) connectToController(addr transport.Address, bindHandler channel.BindHandler) error {
|
|
attributes := map[int32][]byte{}
|
|
|
|
version, err := self.versionProvider.EncoderDecoder().Encode(self.versionProvider.AsVersionInfo())
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error with version header information value: %v", err)
|
|
}
|
|
|
|
attributes[channel.HelloVersionHeader] = version
|
|
|
|
listeners := &ctrl_pb.Listeners{}
|
|
for _, listener := range self.xlinkListeners {
|
|
listeners.Listeners = append(listeners.Listeners, &ctrl_pb.Listener{
|
|
Address: listener.GetAdvertisement(),
|
|
Protocol: listener.GetLinkProtocol(),
|
|
CostTags: listener.GetLinkCostTags(),
|
|
Groups: listener.GetGroups(),
|
|
LocalBinding: listener.GetLocalBinding(),
|
|
})
|
|
}
|
|
|
|
if len(listeners.Listeners) > 0 {
|
|
if buf, err := proto.Marshal(listeners); err != nil {
|
|
return errors.Wrap(err, "unable to marshal Listeners")
|
|
} else {
|
|
attributes[int32(ctrl_pb.ControlHeaders_ListenersHeader)] = buf
|
|
}
|
|
}
|
|
|
|
routerMeta := &ctrl_pb.RouterMetadata{
|
|
Capabilities: []ctrl_pb.RouterCapability{
|
|
ctrl_pb.RouterCapability_LinkManagement,
|
|
},
|
|
}
|
|
|
|
if buf, err := proto.Marshal(routerMeta); err != nil {
|
|
return errors.Wrap(err, "unable to router metadata")
|
|
} else {
|
|
attributes[int32(ctrl_pb.ControlHeaders_RouterMetadataHeader)] = buf
|
|
}
|
|
|
|
var channelRef concurrenz.AtomicValue[channel.Channel]
|
|
reconnectHandler := func() {
|
|
if ch := channelRef.Load(); ch != nil {
|
|
for _, x := range self.xrctrls {
|
|
go x.NotifyOfReconnect(ch)
|
|
}
|
|
}
|
|
}
|
|
|
|
if "" != self.config.Ctrl.LocalBinding {
|
|
logrus.Debugf("Using local interface %s to dial controller", self.config.Ctrl.LocalBinding)
|
|
}
|
|
dialer := channel.NewReconnectingDialerWithHandlerAndLocalBinding(self.config.Id, addr, self.config.Ctrl.LocalBinding, attributes, reconnectHandler)
|
|
|
|
bindHandler = channel.BindHandlers(bindHandler, self.ctrlBindhandler)
|
|
tcfg := transport.Configuration{
|
|
transport.KeyProtocol: "ziti-ctrl",
|
|
transport.KeyCachedProxyConfiguration: self.config.Proxy,
|
|
}
|
|
ch, err := channel.NewChannelWithTransportConfiguration("ctrl", dialer, bindHandler, self.config.Ctrl.Options, tcfg)
|
|
if err != nil {
|
|
return fmt.Errorf("error connecting ctrl (%v)", err)
|
|
}
|
|
channelRef.Store(ch)
|
|
|
|
// If there are multiple controllers we may have to catch up the controllers that connected later
|
|
// with things that have already happened because we had state from other controllers, such as
|
|
// links
|
|
reconnectHandler()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) initializeHealthChecks() (gosundheit.Health, error) {
|
|
checkConfig := self.config.HealthChecks
|
|
logrus.Infof("starting health check with ctrl ping initially after %v, then every %v, timing out after %v",
|
|
checkConfig.CtrlPingCheck.InitialDelay, checkConfig.CtrlPingCheck.Interval, checkConfig.CtrlPingCheck.Timeout)
|
|
|
|
h := gosundheit.New()
|
|
ctrlPinger := &controllerPinger{
|
|
router: self,
|
|
}
|
|
ctrlPingCheck, err := checks.NewPingCheck("controllerPing", ctrlPinger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = h.RegisterCheck(ctrlPingCheck,
|
|
gosundheit.ExecutionPeriod(checkConfig.CtrlPingCheck.Interval),
|
|
gosundheit.ExecutionTimeout(checkConfig.CtrlPingCheck.Timeout),
|
|
gosundheit.InitiallyPassing(false),
|
|
gosundheit.InitialDelay(checkConfig.CtrlPingCheck.InitialDelay),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = h.RegisterCheck(&linkHealthCheck{router: self, minLinks: checkConfig.LinkCheck.MinLinks},
|
|
gosundheit.ExecutionPeriod(checkConfig.LinkCheck.Interval),
|
|
gosundheit.ExecutionTimeout(5*time.Second),
|
|
gosundheit.InitiallyPassing(checkConfig.LinkCheck.MinLinks == 0),
|
|
gosundheit.InitialDelay(checkConfig.LinkCheck.InitialDelay),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
|
|
func (self *Router) RegisterXweb(x xweb.Instance) error {
|
|
if err := self.config.Configure(x); err != nil {
|
|
return err
|
|
}
|
|
if x.Enabled() {
|
|
self.xwebs = append(self.xwebs, x)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *Router) RegisterXWebHandlerFactory(x xweb.ApiHandlerFactory) error {
|
|
return self.xwebFactoryRegistry.Add(x)
|
|
}
|
|
|
|
func (self *Router) getInitialCtrlEndpoints() ([]string, error) {
|
|
log := pfxlog.Logger()
|
|
if self.config.Ctrl.DataDir == "" {
|
|
return nil, errors.New("ctrl DataDir not configured")
|
|
}
|
|
|
|
endpointsFile := path.Join(self.config.Ctrl.DataDir, "endpoints")
|
|
|
|
var endpoints []string
|
|
|
|
if _, err := os.Stat(endpointsFile); err != nil && errors.Is(err, fs.ErrNotExist) {
|
|
log.Infof("controller endpoints file [%v] doesn't exist. Using initial endpoints from config", endpointsFile)
|
|
for _, ep := range self.config.Ctrl.InitialEndpoints {
|
|
endpoints = append(endpoints, ep.String())
|
|
}
|
|
return endpoints, nil
|
|
}
|
|
|
|
log.Infof("loading controller endpoints from [%v]", endpointsFile)
|
|
|
|
b, err := os.ReadFile(endpointsFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endpointCfg := &endpointConfig{}
|
|
|
|
if err = yaml.Unmarshal(b, endpointCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endpoints = endpointCfg.Endpoints
|
|
|
|
if len(endpoints) == 0 {
|
|
return nil, errors.Errorf("no controller endpoints found in [%v], consider deleting file", endpointsFile)
|
|
}
|
|
|
|
return endpoints, nil
|
|
}
|
|
|
|
func (self *Router) UpdateCtrlEndpoints(endpoints []string) {
|
|
log := pfxlog.Logger().WithField("endpoints", endpoints).WithField("filepath", self.config.Ctrl.DataDir)
|
|
if changed := self.ctrls.UpdateControllerEndpoints(endpoints); changed {
|
|
log.Info("Attempting to save file")
|
|
endpointsFile := path.Join(self.config.Ctrl.DataDir, "endpoints")
|
|
|
|
configData := map[string]interface{}{
|
|
"Endpoints": endpoints,
|
|
}
|
|
|
|
if data, err := yaml.Marshal(configData); err != nil {
|
|
log.WithError(err).Error("unable to marshal updated controller endpoints to yaml")
|
|
} else if err = os.WriteFile(endpointsFile, data, 0600); err != nil {
|
|
log.WithError(err).Error("unable to write updated controller endpoints to file")
|
|
}
|
|
}
|
|
}
|
|
|
|
type connectionToggle interface {
|
|
Disconnect() error
|
|
Reconnect() error
|
|
}
|
|
|
|
type controllerPinger struct {
|
|
router *Router
|
|
}
|
|
|
|
func (self *controllerPinger) PingContext(context.Context) error {
|
|
ctrls := self.router.ctrls.GetAll()
|
|
|
|
if len(ctrls) == 0 {
|
|
return errors.New("no control channels established yet")
|
|
}
|
|
|
|
hasGoodConn := false
|
|
|
|
for _, ctrl := range ctrls {
|
|
if !ctrl.IsUnresponsive() {
|
|
hasGoodConn = true
|
|
}
|
|
}
|
|
|
|
if hasGoodConn {
|
|
return nil
|
|
}
|
|
return errors.New("control channels are slow")
|
|
}
|
|
|
|
type endpointConfig struct {
|
|
Endpoints []string `yaml:"Endpoints"`
|
|
}
|
|
|
|
type linkConnDetail struct {
|
|
LocalAddr string `json:"localAddr"`
|
|
RemoteAddr string `json:"remoteAddr"`
|
|
}
|
|
|
|
type linkDetail struct {
|
|
LinkId string `json:"linkId"`
|
|
DestRouterId string `json:"destRouterId"`
|
|
Latency *float64 `json:"latency,omitempty"`
|
|
Addresses map[string]linkConnDetail `json:"addresses"`
|
|
}
|
|
|
|
type linkHealthCheck struct {
|
|
router *Router
|
|
minLinks int
|
|
}
|
|
|
|
func (self *linkHealthCheck) Name() string {
|
|
return "link.health"
|
|
}
|
|
|
|
func (self *linkHealthCheck) Execute(ctx context.Context) (details interface{}, err error) {
|
|
var links []*linkDetail
|
|
|
|
iter := self.router.xlinkRegistry.Iter()
|
|
done := false
|
|
|
|
for !done {
|
|
var currentLink xlink.Xlink
|
|
select {
|
|
case nextLink, ok := <-iter:
|
|
if !ok {
|
|
done = true
|
|
}
|
|
currentLink = nextLink
|
|
case <-ctx.Done():
|
|
done = true
|
|
}
|
|
|
|
if currentLink != nil {
|
|
detail := &linkDetail{
|
|
LinkId: currentLink.Id(),
|
|
DestRouterId: currentLink.DestinationId(),
|
|
Addresses: map[string]linkConnDetail{},
|
|
}
|
|
for _, addr := range currentLink.GetAddresses() {
|
|
detail.Addresses[addr.Id] = linkConnDetail{
|
|
LocalAddr: addr.LocalAddr,
|
|
RemoteAddr: addr.RemoteAddr,
|
|
}
|
|
}
|
|
latencyMetric := self.router.metricsRegistry.Histogram("link." + currentLink.Id() + ".latency")
|
|
if latencyMetric != nil {
|
|
latency := latencyMetric.(metrics2.Histogram).Mean()
|
|
detail.Latency = &latency
|
|
}
|
|
links = append(links, detail)
|
|
}
|
|
}
|
|
if len(links) < self.minLinks {
|
|
return links, errors.Errorf("link count %v less than configured minimum of %v", len(links), self.minLinks)
|
|
}
|
|
return links, nil
|
|
}
|