Files
ziti/controller/handler_ctrl/accept.go
T
Paul Lorenz 48e4d4a224 Manage router link configuration via the controller. Fixes #4004
- adds a link ConfigHandler (router/link FactoryRegistry) that applies router.link.v1 config: Apply rebuilds the listener/dialer set wholesale, and established Xlinks survive because Listener.Close() only closes the accept loop
- translates local link: YAML into router.link.v1 JSON and pushes it through the managed-config registry at startup
- adds the UpdateLinkListeners ctrl message so the router republishes its listener set to the controller on change; the controller re-fans via the existing PeerStateChange path
- re-evaluates dialers on link group and listener changes via RescanForDialOpportunities

For #3743.
2026-08-13 15:37:24 -04:00

210 lines
7.9 KiB
Go

/*
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_ctrl
import (
"time"
"github.com/google/uuid"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/channel/v5"
"github.com/openziti/ziti/v2/common/capabilities"
"github.com/openziti/ziti/v2/common/ctrlchan"
"github.com/openziti/ziti/v2/common/pb/ctrl_pb"
"github.com/openziti/ziti/v2/controller/change"
"github.com/openziti/ziti/v2/controller/network"
"github.com/openziti/ziti/v2/controller/xctrl"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
)
type CtrlAccepter struct {
network *network.Network
xctrls []xctrl.Xctrl
options *channel.Options
heartbeatOptions *channel.HeartbeatOptions
traceHandler *channel.TraceHandler
}
func NewCtrlAccepter(network *network.Network,
xctrls []xctrl.Xctrl,
options *channel.Options,
heartbeatOptions *channel.HeartbeatOptions,
traceHandler *channel.TraceHandler) *CtrlAccepter {
return &CtrlAccepter{
network: network,
xctrls: xctrls,
options: options,
heartbeatOptions: heartbeatOptions,
traceHandler: traceHandler,
}
}
// NewMultiListener returns a HelloAcceptor that handles both grouped (multi-underlay) and
// ungrouped (single underlay) connections from routers. As a HelloAcceptor it defers the
// hello acknowledgement until the group is registered, closing the race where a second
// underlay for the same group could arrive before the group was known.
func (self *CtrlAccepter) NewMultiListener() channel.HelloAcceptor {
return channel.NewMultiListener(self.HandleGroupedUnderlay, self.AcceptUnderlay)
}
// HandleGroupedUnderlay handles incoming grouped connections from routers that support
// multi-underlay control channels. It creates a MultiChannel with ListenerCtrlChannel.
func (self *CtrlAccepter) HandleGroupedUnderlay(underlay channel.Underlay, closeCallback func()) (channel.Channel, error) {
if _, hasSecret := underlay.Headers()[channel.GroupSecretHeader]; !hasSecret {
underlay.Headers()[channel.GroupSecretHeader] = []byte(uuid.NewString())
}
listenerCtrlChan := ctrlchan.NewListenerCtrlChannel()
multiConfig := channel.Config{
LogicalName: "ctrl/" + underlay.Id(),
Options: self.options,
Underlay: underlay,
Binder: channel.MakeBinder(channel.BindHandlerF(func(binding channel.Binding) error {
binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) {
closeCallback()
}))
return self.Bind(binding)
})),
Senders: listenerCtrlChan,
MessageSourceProvider: listenerCtrlChan,
UnderlayEventListeners: []channel.UnderlayEventListener{listenerCtrlChan},
// Multi-underlay-capable so the high/low-priority underlays are accepted;
// MinTotalUnderlays closes the channel only when its last underlay is lost.
Constraints: listenerCtrlChan.GetConstraints(),
MinTotalUnderlays: 1,
}
mc, err := channel.NewChannel(&multiConfig)
if err != nil {
pfxlog.Logger().WithError(err).Errorf("failure accepting ctrl channel %v with multi-underlay", underlay.Label())
return nil, err
}
return mc, nil
}
// AcceptUnderlay handles incoming ungrouped connections from routers that don't support
// multi-underlay control channels (backward compatibility).
func (self *CtrlAccepter) AcceptUnderlay(underlay channel.Underlay) error {
_, err := self.HandleGroupedUnderlay(underlay, func() {})
return err
}
func (self *CtrlAccepter) Bind(binding channel.Binding) error {
binding.GetChannel().SetLogicalName(binding.GetChannel().Id())
ch := binding.GetChannel()
log := pfxlog.Logger().WithField("routerId", ch.Id())
// Use a new copy of the router instance each time we connect. That way we can tell on disconnect
// if we're working with the right connection, in case connects and disconnects happen quickly.
// It also means that the channel and connected time fields don't change and we don't have to protect them
r, err := self.network.GetReloadedRouter(ch.Id())
if err != nil {
return err
}
if r == nil {
return errors.Errorf("no router with id [%v] found, closing connection", ch.Id())
}
var ctrlChanListeners map[string][]string
if ch.Underlay().Headers() != nil {
if versionValue, found := ch.Underlay().Headers()[channel.HelloVersionHeader]; found {
if versionInfo, err := self.network.VersionProvider.EncoderDecoder().Decode(versionValue); err == nil {
r.VersionInfo = versionInfo
log = log.WithField("version", r.VersionInfo.Version).
WithField("revision", r.VersionInfo.Revision).
WithField("buildDate", r.VersionInfo.BuildDate).
WithField("os", r.VersionInfo.OS).
WithField("arch", r.VersionInfo.Arch)
} else {
return errors.Wrap(err, "could not parse version info from router hello, not accepting router connection")
}
} else {
return errors.New("no version info header, not accepting router connection")
}
r.SetLinkListeners(nil)
headers := ch.Underlay().Headers()
// Determine header locations based on router capabilities. 2.0+ routers
// send a CapabilitiesHeader with RouterMultiChannel set and use header IDs
// in the 1000+ range. Pre-2.0 routers use legacy IDs (10-12) and don't
// send a CapabilitiesHeader.
r.Capabilities = capabilities.GetCapabilities[capabilities.RouterCapability](headers)
useNewHeaders := r.Capabilities.IsSet(capabilities.RouterMultiChannel)
listenersHeaderId := ctrl_pb.LegacyListenersHeader
if useNewHeaders {
listenersHeaderId = int32(ctrl_pb.ControlHeaders_ListenersHeader)
}
if val, found := headers[listenersHeaderId]; found {
listeners := &ctrl_pb.Listeners{}
if err = proto.Unmarshal(val, listeners); err != nil {
log.WithError(err).Error("unable to unmarshall listeners value")
} else {
r.SetLinkListeners(listeners.Listeners)
for _, listener := range listeners.Listeners {
log.WithField("address", listener.GetAddress()).
WithField("protocol", listener.GetProtocol()).
Debug("router listener")
}
}
} else {
log.Debug("no advertised listeners")
}
if val, found := ch.Underlay().Headers()[int32(ctrl_pb.ControlHeaders_CtrlChanListenersHeader)]; found {
ctrlListeners := &ctrl_pb.CtrlChanListeners{}
if err = proto.Unmarshal(val, ctrlListeners); err != nil {
log.WithError(err).Error("unable to unmarshal ctrl chan listeners value")
} else {
ctrlChanListeners = make(map[string][]string, len(ctrlListeners.Listeners))
for _, listener := range ctrlListeners.Listeners {
ctrlChanListeners[listener.Address] = listener.Groups
}
}
}
changeCtx := change.NewControlChannelChange(r.Id, r.Name, "router.connect", ch)
self.network.Router.UpdateCtrlChanListeners(r, ctrlChanListeners, changeCtx)
} else {
return errors.New("channel provided no headers, not accepting router connection as version info not provided")
}
r.Control = ch.GetSenders().(ctrlchan.CtrlChannel)
// Record the channel before the downstream bind handlers run: GetCtrlHandlers builds
// handlers (e.g. NewConnectEventsHandler) that dereference the ctrl channel's Channel().
// UnderlayAdded fires after this bind handler, so without InitChannel here those derefs
// would hit a nil channel.
r.Control.InitChannel(ch)
r.ConnectTime = time.Now()
if err = newBindHandler(self.heartbeatOptions, r, self.network, self.xctrls).BindChannel(binding); err != nil {
return errors.Wrap(err, "error binding router")
}
if self.traceHandler != nil {
binding.AddPeekHandler(self.traceHandler)
}
log.Info("accepted new router connection")
self.network.ConnectRouter(r)
return nil
}