mirror of
https://github.com/openziti/ziti.git
synced 2026-09-11 21:38:58 +00:00
99ca242ecc
The controller decided which of two racing connections for a router was current
by comparing router instances, but loaded one per connect by evicting the router
cache and reading back through it. Two connects could both evict, and whichever
read second was handed the instance the first had just published. A shared
instance makes the two connections indistinguishable: the connect path cannot
reject the second into an occupied slot, and when either channel dies the
disconnect path finds itself current and tears down the registration the other is
still using. The surviving channel is never re-bound, so the router stays
connected at the transport layer while absent from the model, unable to recover.
Connect and disconnect were also unserialized, so a stale or superseded
disconnect could interleave with a live connection and take its links with it.
- serializes a router's connect and disconnect with a per-router striped lock
- keeps at most one connection per router: a connect into an occupied slot is
rejected via an error from ConnectRouter, so the bind fails and NewChannel
closes it without starting rx or registering it, and the occupant is displaced;
the router redials into the freed slot
- displaces an occupant by closing its channel and also invoking the teardown
directly, since a channel that is already closed never fires its close handler
again; without this a dead but still registered connection holds the slot
forever and every redial is rejected against a slot nothing can free
- refuses a connect whose control channel is already closed rather than
registering it, so a connection no disconnect could ever remove is never
published
- gives every connection its own router instance via RouterManager.NewCtrlChanRouter,
read through readUncached so the cache neither supplies nor receives it, which
is what makes comparing instances meaningful
- moves recording the channel and connect time out of the accept path, so a
caller cannot attach the wrong channel or forget to attach one
- serializes link publication with that teardown on the same per-router stripe.
Validating currency and then publishing without it is a check-then-act: a
report can find the connection current and, by the time it reaches the link
manager, the teardown has already snapshotted and cleared the router's links,
so the link is recreated after everything that would have removed it. It is
then absent from the router's own index while still in the link table with a
disconnected source, and a reconnect reporting the same iteration can adopt
that stale source instead of rebuilding the link
- guards the entire DisconnectRouter teardown by connection currency, all or
nothing, and clears the connected flag and link index only when the
registration was actually given up, with the flag cleared under the same shard
lock as the map removal so the two cannot be observed disagreeing; the connected flag decides whether the
controller accepts a router's link reports, so clearing it for the wrong
connection silences a router that is up and reporting
- reduces MarkConnected to publishing the connection; the takeover-close moves
into ConnectRouter's reject path
- makes the per-router unlock idempotent so callers can defer it as a leak-safety
net and still unlock early before closing a channel outside the lock
- stops the replaced RouterSender in routerTxMap.Add so a takeover does not leak
the old sender's goroutine when the broker's asynchronous RouterDisconnected
loses the race to the redial's RouterConnected
- discards pending peer state changes for a router whose channel has closed,
since sending on one fails immediately and the failed send is retried as soon
as the event loop turns, spinning the loop and flooding the log
- queues the peer-state send-done event on every path, so a missing channel can
no longer leave sendInProgress set and stall that router's updates permanently
- resolves a router's version from its connected instance when validating link
conn info, since the version arrives in the hello and so is absent from an
instance loaded from the database
- normalizes both endpoints to the connected instance in shortestPath, which is
keyed and compared by pointer and so treated an endpoint held as any other
instance of the same router as absent from the graph, reporting a router as
unroutable from itself. That worked before only because the connect path
published its instance into the router cache, so a cache read and the connected
map returned the same object; nothing stated the requirement
- configures test logging once per package in TestMain, so a test no longer
writes global logger state while a previous test's shutdown logging reads it
Original issue: #4196.
(cherry picked from commit 647c4daa1e)
1759 lines
56 KiB
Go
1759 lines
56 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 network
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"runtime/debug"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/channel/v4/protobufs"
|
|
"github.com/openziti/channel/v4"
|
|
"github.com/openziti/foundation/v2/concurrenz"
|
|
"github.com/openziti/foundation/v2/debugz"
|
|
"github.com/openziti/foundation/v2/goroutines"
|
|
"github.com/openziti/foundation/v2/versions"
|
|
"github.com/openziti/identity"
|
|
"github.com/openziti/metrics"
|
|
"github.com/openziti/metrics/metrics_pb"
|
|
"github.com/openziti/ziti/v2/controller/storage/boltz"
|
|
"github.com/openziti/ziti/v2/controller/storage/objectz"
|
|
"github.com/openziti/ziti/v2/common/ctrl_msg"
|
|
"github.com/openziti/ziti/v2/common/inspect"
|
|
"github.com/openziti/ziti/v2/common/logcontext"
|
|
fabricMetrics "github.com/openziti/ziti/v2/common/metrics"
|
|
"github.com/openziti/ziti/v2/common/pb/cmd_pb"
|
|
"github.com/openziti/ziti/v2/common/pb/ctrl_pb"
|
|
"github.com/openziti/ziti/v2/common/pb/mgmt_pb"
|
|
"github.com/openziti/ziti/v2/common/trace"
|
|
"github.com/openziti/ziti/v2/controller/command"
|
|
"github.com/openziti/ziti/v2/controller/config"
|
|
"github.com/openziti/ziti/v2/controller/db"
|
|
"github.com/openziti/ziti/v2/controller/event"
|
|
"github.com/openziti/ziti/v2/controller/idgen"
|
|
"github.com/openziti/ziti/v2/controller/model"
|
|
"github.com/openziti/ziti/v2/controller/xt"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/teris-io/shortid"
|
|
"go.etcd.io/bbolt"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
const SmartRerouteAttempt = 99969996
|
|
|
|
// Config provides the values needed to create a Network instance
|
|
type Config interface {
|
|
GetId() *identity.TokenId
|
|
GetMetricsRegistry() metrics.Registry
|
|
GetOptions() *config.NetworkConfig
|
|
GetCommandDispatcher() command.Dispatcher
|
|
GetDb() boltz.Db
|
|
GetVersionProvider() versions.VersionProvider
|
|
GetEventDispatcher() event.Dispatcher
|
|
GetCloseNotify() <-chan struct{}
|
|
}
|
|
|
|
type InspectTarget func(string) (bool, *string, error)
|
|
|
|
// CtrlDialerValidator is a function that validates ctrl dialer states and returns per-router details.
|
|
type CtrlDialerValidator func() ([]*mgmt_pb.ControllerDialerDetails, error)
|
|
|
|
type Network struct {
|
|
*model.Managers
|
|
env model.Env
|
|
nodeId string
|
|
options *config.NetworkConfig
|
|
routeSenderController *routeSenderController
|
|
eventDispatcher event.Dispatcher
|
|
traceController trace.Controller
|
|
routerPresenceHandlers concurrenz.CopyOnWriteSlice[model.RouterPresenceHandler]
|
|
capabilities []string
|
|
closeNotify <-chan struct{}
|
|
watchdogCh chan struct{}
|
|
lock sync.Mutex
|
|
strategyRegistry xt.Registry
|
|
lastSnapshot time.Time
|
|
metricsRegistry metrics.Registry
|
|
VersionProvider versions.VersionProvider
|
|
|
|
serviceEventMetrics metrics.UsageRegistry
|
|
serviceDialSuccessCounter metrics.IntervalCounter
|
|
serviceDialFailCounter metrics.IntervalCounter
|
|
serviceDialTimeoutCounter metrics.IntervalCounter
|
|
serviceDialOtherErrorCounter metrics.IntervalCounter
|
|
|
|
serviceTerminatorTimeoutCounter metrics.IntervalCounter
|
|
serviceTerminatorConnectionRefusedCounter metrics.IntervalCounter
|
|
serviceInvalidTerminatorCounter metrics.IntervalCounter
|
|
serviceMisconfiguredTerminatorCounter metrics.IntervalCounter
|
|
|
|
config Config
|
|
|
|
Inspections *InspectionsManager
|
|
RouterMessaging *RouterMessaging
|
|
inspectionTargets concurrenz.CopyOnWriteSlice[InspectTarget]
|
|
ctrlDialerValidator CtrlDialerValidator
|
|
}
|
|
|
|
func NewNetwork(config Config, env model.Env) (*Network, error) {
|
|
metricsConfig := metrics.DefaultUsageRegistryConfig(config.GetId().Token, config.GetCloseNotify())
|
|
if config.GetOptions().IntervalAgeThreshold != 0 {
|
|
metricsConfig.IntervalAgeThreshold = config.GetOptions().IntervalAgeThreshold
|
|
logrus.Infof("set interval age threshold to '%v'", config.GetOptions().IntervalAgeThreshold)
|
|
}
|
|
serviceEventMetrics := metrics.NewUsageRegistry(metricsConfig)
|
|
|
|
network := &Network{
|
|
env: env,
|
|
Managers: env.GetManagers(),
|
|
nodeId: config.GetId().Token,
|
|
options: config.GetOptions(),
|
|
routeSenderController: newRouteSenderController(),
|
|
eventDispatcher: config.GetEventDispatcher(),
|
|
traceController: trace.NewController(config.GetCloseNotify()),
|
|
closeNotify: config.GetCloseNotify(),
|
|
watchdogCh: make(chan struct{}, 1),
|
|
strategyRegistry: xt.GlobalRegistry(),
|
|
lastSnapshot: time.Now().Add(-time.Hour),
|
|
metricsRegistry: config.GetMetricsRegistry(),
|
|
VersionProvider: config.GetVersionProvider(),
|
|
|
|
serviceEventMetrics: serviceEventMetrics,
|
|
serviceDialSuccessCounter: serviceEventMetrics.IntervalCounter("service.dial.success", time.Minute),
|
|
serviceDialFailCounter: serviceEventMetrics.IntervalCounter("service.dial.fail", time.Minute),
|
|
serviceDialTimeoutCounter: serviceEventMetrics.IntervalCounter("service.dial.timeout", time.Minute),
|
|
serviceDialOtherErrorCounter: serviceEventMetrics.IntervalCounter("service.dial.error_other", time.Minute),
|
|
|
|
serviceTerminatorTimeoutCounter: serviceEventMetrics.IntervalCounter("service.dial.terminator.timeout", time.Minute),
|
|
serviceTerminatorConnectionRefusedCounter: serviceEventMetrics.IntervalCounter("service.dial.terminator.connection_refused", time.Minute),
|
|
serviceInvalidTerminatorCounter: serviceEventMetrics.IntervalCounter("service.dial.terminator.invalid", time.Minute),
|
|
serviceMisconfiguredTerminatorCounter: serviceEventMetrics.IntervalCounter("service.dial.terminator.misconfigured", time.Minute),
|
|
|
|
config: config,
|
|
}
|
|
|
|
env.GetManagers().Command.Decoders.RegisterF(int32(cmd_pb.CommandType_SyncSnapshot), network.decodeSyncSnapshotCommand)
|
|
|
|
routerCommPool, err := network.createRouterCommPool(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
network.Inspections = NewInspectionsManager(network)
|
|
network.RouterMessaging = NewRouterMessaging(env, routerCommPool)
|
|
|
|
env.GetManagers().Router.Store.AddEntityIdListener(network.HandleRouterDelete, boltz.EntityDeletedAsync)
|
|
|
|
network.AddCapability("ziti.fabric")
|
|
network.showOptions()
|
|
network.relayControllerMetrics()
|
|
network.AddRouterPresenceHandler(network.RouterMessaging)
|
|
go network.RouterMessaging.run()
|
|
|
|
return network, nil
|
|
}
|
|
|
|
func (self *Network) HandleRouterDelete(id string) {
|
|
self.routerDeleted(id)
|
|
self.RouterMessaging.RouterDeleted(id)
|
|
}
|
|
|
|
func (self *Network) decodeSyncSnapshotCommand(_ int32, data []byte) (command.Command, error) {
|
|
msg := &cmd_pb.SyncSnapshotCommand{}
|
|
if err := proto.Unmarshal(data, msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := &command.SyncSnapshotCommand{
|
|
TimelineId: msg.SnapshotId,
|
|
Snapshot: msg.Snapshot,
|
|
ClusterId: msg.ClusterId,
|
|
SnapshotSink: self.RestoreSnapshot,
|
|
}
|
|
|
|
return cmd, nil
|
|
}
|
|
|
|
func routerCommunicationsWorker(_ uint32, f func()) {
|
|
f()
|
|
}
|
|
|
|
func (network *Network) createRouterCommPool(config Config) (goroutines.Pool, error) {
|
|
poolConfig := goroutines.PoolConfig{
|
|
QueueSize: config.GetOptions().RouterComm.QueueSize,
|
|
MinWorkers: 0,
|
|
MaxWorkers: config.GetOptions().RouterComm.MaxWorkers,
|
|
IdleTime: 30 * time.Second,
|
|
CloseNotify: config.GetCloseNotify(),
|
|
PanicHandler: func(err interface{}) {
|
|
pfxlog.Logger().WithField(logrus.ErrorKey, err).WithField("backtrace", string(debug.Stack())).Error("panic during message send to router")
|
|
},
|
|
WorkerFunction: routerCommunicationsWorker,
|
|
}
|
|
|
|
fabricMetrics.ConfigureGoroutinesPoolMetrics(&poolConfig, config.GetMetricsRegistry(), "pool.router.messaging")
|
|
|
|
pool, err := goroutines.NewPool(poolConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating router messaging pool (%w)", err)
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
func (network *Network) relayControllerMetrics() {
|
|
go func() {
|
|
timer := time.NewTicker(network.options.MetricsReportInterval)
|
|
defer timer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-timer.C:
|
|
if msg := network.metricsRegistry.Poll(); msg != nil {
|
|
network.eventDispatcher.AcceptMetricsMsg(msg)
|
|
}
|
|
case <-network.closeNotify:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (network *Network) InitServiceCounterDispatch(handler metrics.Handler) {
|
|
network.serviceEventMetrics.StartReporting(handler, network.GetOptions().MetricsReportInterval, 10)
|
|
}
|
|
|
|
func (network *Network) GetAppId() string {
|
|
return network.nodeId
|
|
}
|
|
|
|
func (network *Network) GetOptions() *config.NetworkConfig {
|
|
return network.options
|
|
}
|
|
|
|
func (network *Network) GetDb() boltz.Db {
|
|
return network.config.GetDb()
|
|
}
|
|
|
|
func (network *Network) GetStores() *db.Stores {
|
|
return network.env.GetStores()
|
|
}
|
|
|
|
func (network *Network) GetConnectedRouter(routerId string) *model.Router {
|
|
return network.Router.GetConnected(routerId)
|
|
}
|
|
|
|
// NewCtrlChanRouter returns the Router instance representing a new control-channel connection, with the
|
|
// channel recorded on it. Each connection gets its own instance, which is what lets the connect and
|
|
// disconnect paths tell two racing connections for one router apart.
|
|
func (network *Network) NewCtrlChanRouter(ch channel.Channel) (*model.Router, error) {
|
|
return network.Router.NewCtrlChanRouter(ch)
|
|
}
|
|
|
|
func (network *Network) GetRouter(routerId string) (*model.Router, error) {
|
|
return network.Router.Read(routerId)
|
|
}
|
|
|
|
func (network *Network) AllConnectedRouters() []*model.Router {
|
|
return network.Router.AllConnected()
|
|
}
|
|
|
|
func (network *Network) GetLink(linkId string) (*model.Link, bool) {
|
|
return network.Link.Get(linkId)
|
|
}
|
|
|
|
func (network *Network) GetAllLinks() []*model.Link {
|
|
return network.Link.All()
|
|
}
|
|
|
|
func (network *Network) GetAllLinksForRouter(routerId string) []*model.Link {
|
|
r := network.GetConnectedRouter(routerId)
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return r.GetLinks()
|
|
}
|
|
|
|
func (network *Network) GetCircuit(circuitId string) (*model.Circuit, bool) {
|
|
return network.Circuit.Get(circuitId)
|
|
}
|
|
|
|
func (network *Network) GetAllCircuits() []*model.Circuit {
|
|
return network.Circuit.All()
|
|
}
|
|
|
|
func (network *Network) GetCircuitStore() *objectz.ObjectStore[*model.Circuit] {
|
|
return network.Circuit.GetStore()
|
|
}
|
|
|
|
func (network *Network) GetLinkStore() *objectz.ObjectStore[*model.Link] {
|
|
return network.Link.GetStore()
|
|
}
|
|
|
|
func (network *Network) RouteResult(rs *RouteStatus) bool {
|
|
return network.routeSenderController.forwardRouteResult(rs)
|
|
}
|
|
|
|
func (network *Network) newRouteSender(circuitId string) *routeSender {
|
|
rs := newRouteSender(circuitId, network.options.RouteTimeout, network, network.Terminator)
|
|
network.routeSenderController.addRouteSender(rs)
|
|
return rs
|
|
}
|
|
|
|
func (network *Network) removeRouteSender(rs *routeSender) {
|
|
network.routeSenderController.removeRouteSender(rs)
|
|
}
|
|
|
|
func (network *Network) GetEventDispatcher() event.Dispatcher {
|
|
return network.eventDispatcher
|
|
}
|
|
|
|
func (network *Network) GetTraceController() trace.Controller {
|
|
return network.traceController
|
|
}
|
|
|
|
func (network *Network) GetMetricsRegistry() metrics.Registry {
|
|
return network.metricsRegistry
|
|
}
|
|
|
|
func (network *Network) GetServiceEventsMetricsRegistry() metrics.UsageRegistry {
|
|
return network.serviceEventMetrics
|
|
}
|
|
|
|
func (network *Network) GetCloseNotify() <-chan struct{} {
|
|
return network.closeNotify
|
|
}
|
|
|
|
func (network *Network) ConnectedRouter(id string) bool {
|
|
return network.Router.IsConnected(id)
|
|
}
|
|
|
|
var (
|
|
// ErrConnectRejected indicates a router connect was rejected because another connection for the same
|
|
// router is already current. It is returned by ConnectRouter and propagated out of the bind handler so
|
|
// NewChannel closes the rejected connection's underlay without starting rx or registering it; the
|
|
// router then redials.
|
|
ErrConnectRejected = errors.New("router connect rejected: another connection is already current")
|
|
|
|
// ErrConnectChannelClosed indicates a router connect was refused because its control channel was
|
|
// already closed by the time the connect decision was made, so the connection must not be registered.
|
|
ErrConnectChannelClosed = errors.New("router connect rejected: control channel already closed")
|
|
)
|
|
|
|
// IsConnectRejected reports whether err is (or wraps) a connect refusal that the router recovers from by
|
|
// redialing, so the accept path can log it at info rather than treating it as a bind failure.
|
|
func IsConnectRejected(err error) bool {
|
|
return errors.Is(err, ErrConnectRejected) || errors.Is(err, ErrConnectChannelClosed)
|
|
}
|
|
|
|
// ConnectRouter registers r as the current connection for its router id, serialized per router. If the
|
|
// slot is already held by a different connection it rejects this one (returning ErrConnectRejected) and
|
|
// displaces the occupant so its teardown runs; the router redials into the freed slot. A connection whose
|
|
// channel is already closed is refused outright (ErrConnectChannelClosed) rather than registered. There is
|
|
// at most one connection per router in the connected map at a time.
|
|
func (network *Network) ConnectRouter(r *model.Router) error {
|
|
unlock := network.Router.LockConnectFor(r.Id)
|
|
defer unlock() // leak-safety net; idempotent, so the explicit unlocks below are the ones that matter
|
|
|
|
if cur := network.Router.GetConnected(r.Id); cur != nil && cur != r {
|
|
// Displace the occupant outside the lock: the teardown acquires the stripe itself (we have
|
|
// released it), so there is no reentrant self-deadlock.
|
|
unlock()
|
|
network.displaceConnection(cur)
|
|
return ErrConnectRejected
|
|
}
|
|
|
|
// Its close handler has already run and never fires again, so nothing would remove it from the
|
|
// connected map and every redial would bounce off a slot that can never be freed.
|
|
if r.Control == nil || r.Control.IsClosed() {
|
|
return ErrConnectChannelClosed
|
|
}
|
|
|
|
network.Link.BuildRouterLinks(r)
|
|
network.Router.MarkConnected(r)
|
|
|
|
for _, h := range network.routerPresenceHandlers.Value() {
|
|
if syncCapableHandler, ok := h.(model.SyncRouterPresenceHandler); ok && syncCapableHandler.InvokeRouterConnectedSynchronously() {
|
|
h.RouterConnected(r)
|
|
} else {
|
|
go h.RouterConnected(r)
|
|
}
|
|
}
|
|
unlock()
|
|
|
|
go network.ValidateTerminators(r)
|
|
return nil
|
|
}
|
|
|
|
func (network *Network) ValidateTerminators(r *model.Router) {
|
|
logger := pfxlog.Logger().WithField("routerId", r.Id)
|
|
result, err := network.Terminator.Query(fmt.Sprintf(`router.id = "%v" limit none`, r.Id))
|
|
if err != nil {
|
|
logger.WithError(err).Error("failed to get terminators for router")
|
|
return
|
|
}
|
|
|
|
logger.Debugf("%v terminators to validate", len(result.Entities))
|
|
if len(result.Entities) == 0 {
|
|
return
|
|
}
|
|
|
|
network.RouterMessaging.ValidateRouterTerminators(result.Entities)
|
|
}
|
|
|
|
type LinkValidationCallback func(detail *mgmt_pb.RouterLinkDetails)
|
|
|
|
func (n *Network) ValidateLinks(filter string, cb LinkValidationCallback) (int64, func(), error) {
|
|
result, err := n.Router.BaseList(filter)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
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.ValidateRouterLinks(connectedRouter, cb)
|
|
}()
|
|
} else {
|
|
n.reportRouterLinksError(router, errors.New("router not connected"), cb)
|
|
}
|
|
}
|
|
}
|
|
|
|
return int64(len(result.Entities)), evalF, nil
|
|
}
|
|
|
|
type SdkTerminatorValidationCallback func(detail *mgmt_pb.RouterSdkTerminatorsDetails)
|
|
|
|
func (n *Network) ValidateRouterSdkTerminators(filter string, cb SdkTerminatorValidationCallback) (int64, func(), error) {
|
|
result, err := n.Router.BaseList(filter)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
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.Router.ValidateRouterSdkTerminators(connectedRouter, cb)
|
|
}()
|
|
} else {
|
|
n.Router.ReportRouterSdkTerminatorsError(router, errors.New("router not connected"), cb)
|
|
}
|
|
}
|
|
}
|
|
|
|
return int64(len(result.Entities)), evalF, nil
|
|
}
|
|
|
|
type ErtTerminatorValidationCallback func(detail *mgmt_pb.RouterErtTerminatorsDetails)
|
|
|
|
func (n *Network) ValidateRouterErtTerminators(filter string, cb ErtTerminatorValidationCallback) (int64, func(), error) {
|
|
result, err := n.Router.BaseList(filter)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
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.Router.ValidateRouterErtTerminators(connectedRouter, cb)
|
|
}()
|
|
} else {
|
|
n.Router.ReportRouterErtTerminatorsError(router, errors.New("router not connected"), cb)
|
|
}
|
|
}
|
|
}
|
|
|
|
return int64(len(result.Entities)), evalF, nil
|
|
}
|
|
|
|
// isCurrentConnection reports whether r is still the router's current, connected connection, by pointer
|
|
// identity against the connected map (mirrors the check in NotifyExistingLink). A stale or superseded
|
|
// connection returns false.
|
|
func (network *Network) isCurrentConnection(r *model.Router) bool {
|
|
return network.Router.GetConnected(r.Id) == r && r.Connected.Load()
|
|
}
|
|
|
|
// displaceConnection removes cur, the connection occupying its router's connected slot, so that a redial
|
|
// can take the slot. Closing the channel is not sufficient on its own: if it is already closed, its close
|
|
// handler has already run and will never run again, so nothing would remove cur and every subsequent
|
|
// connect would be rejected against a slot that can never be freed. The teardown is therefore also
|
|
// invoked directly; it is gated on connection currency, so it is a no-op once the close handler has
|
|
// cleared the slot. Must be called with the router's connect stripe released, since the teardown
|
|
// acquires it.
|
|
func (network *Network) displaceConnection(cur *model.Router) {
|
|
if ch := cur.Control; ch != nil && !ch.IsClosed() {
|
|
if err := ch.Close(); err != nil {
|
|
pfxlog.Logger().WithError(err).WithField("routerId", cur.Id).
|
|
Error("error closing superseded control channel while rejecting connect")
|
|
}
|
|
}
|
|
network.DisconnectRouter(cur)
|
|
}
|
|
|
|
func (network *Network) DisconnectRouter(r *model.Router) {
|
|
// Lock-free pre-check: a stale/superseded disconnect (e.g. the old connection after a takeover) has
|
|
// nothing to tear down and must not touch the live connection's state; bail without blocking.
|
|
if !network.isCurrentConnection(r) {
|
|
return
|
|
}
|
|
|
|
unlock := network.Router.LockConnectFor(r.Id)
|
|
defer unlock()
|
|
|
|
// Re-check under the stripe: a newer connection may have taken over between the pre-check and the
|
|
// lock. The teardown is all-or-nothing and must not run against a superseded connection.
|
|
if !network.isCurrentConnection(r) {
|
|
return
|
|
}
|
|
|
|
// Snapshot the router's links before marking it disconnected: MarkDisconnected clears the
|
|
// router's link set (routerLinks.Clear()), so a later r.GetLinks() would return nothing and
|
|
// the link-removal/reroute cascade below would be skipped entirely.
|
|
links := r.GetLinks()
|
|
|
|
// Mark the router disconnected before the RerouteLink cascade, so reroute and everything it
|
|
// calls (shortestPath, connected-map reads) sees the dying router as gone. Otherwise reroute
|
|
// runs while the router still appears connected and can compute a replacement path through the
|
|
// very router that is being removed.
|
|
network.Router.MarkDisconnected(r)
|
|
|
|
// remove Links for Router, rerouting circuits off any that were connected
|
|
for _, l := range links {
|
|
wasConnected := l.CurrentState().Mode == model.Connected
|
|
if l.Src.Id == r.Id {
|
|
network.Link.Remove(l)
|
|
}
|
|
if wasConnected {
|
|
network.RerouteLink(l)
|
|
}
|
|
}
|
|
|
|
for _, h := range network.routerPresenceHandlers.Value() {
|
|
h.RouterDisconnected(r)
|
|
}
|
|
}
|
|
|
|
func (network *Network) NotifyExistingLink(srcRouter *model.Router, reportedLink *ctrl_pb.RouterLinks_RouterLink) {
|
|
log := pfxlog.Logger().
|
|
WithField("routerId", srcRouter.Id).
|
|
WithField("linkId", reportedLink.Id).
|
|
WithField("destRouterId", reportedLink.DestRouterId).
|
|
WithField("iteration", reportedLink.Iteration)
|
|
|
|
// Publish under the stripe DisconnectRouter holds: checking currency and then publishing without it
|
|
// lets a report recreate a link after the teardown has snapshotted and cleared it. Events go out after
|
|
// the unlock, since a dispatcher may be slow and this stripe is shared with connect and disconnect.
|
|
unlock := network.Router.LockConnectFor(srcRouter.Id)
|
|
|
|
src := network.Router.GetConnected(srcRouter.Id)
|
|
if src == nil {
|
|
unlock()
|
|
log.Info("ignoring links message processed after router disconnected")
|
|
return
|
|
}
|
|
|
|
if src != srcRouter || !srcRouter.Connected.Load() {
|
|
unlock()
|
|
log.Info("ignoring links message processed from old router connection")
|
|
return
|
|
}
|
|
|
|
dst := network.Router.GetConnected(reportedLink.DestRouterId)
|
|
link, created := network.Link.RouterReportedLink(reportedLink, src, dst)
|
|
|
|
unlock()
|
|
|
|
if dst == nil {
|
|
network.NotifyLinkIdEvent(reportedLink.Id, event.LinkFromRouterDisconnectedDest)
|
|
}
|
|
|
|
if created {
|
|
network.NotifyLinkEvent(link, event.LinkFromRouterNew)
|
|
log.Info("router reported link added")
|
|
} else {
|
|
network.NotifyLinkEvent(link, event.LinkFromRouterKnown)
|
|
log.Info("router reported link already known")
|
|
}
|
|
}
|
|
|
|
func (network *Network) LinkFaulted(link *model.Link, dupe bool) {
|
|
wasUsable := link.IsUsable()
|
|
|
|
link.SetState(model.Failed)
|
|
if dupe {
|
|
network.NotifyLinkEvent(link, event.LinkDuplicate)
|
|
} else {
|
|
network.NotifyLinkEvent(link, event.LinkFault)
|
|
}
|
|
|
|
pfxlog.Logger().WithField("linkId", link.Id).Info("removing failed link")
|
|
network.Link.Remove(link)
|
|
|
|
if wasUsable {
|
|
network.RerouteLink(link)
|
|
}
|
|
}
|
|
|
|
func (network *Network) VerifyRouter(routerId string, fingerprints []string) error {
|
|
router, err := network.GetRouter(routerId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
routerFingerprint := router.Fingerprint
|
|
if routerFingerprint == nil {
|
|
return fmt.Errorf("invalid router %v, not yet enrolled", routerId)
|
|
}
|
|
|
|
for _, fp := range fingerprints {
|
|
if fp == *routerFingerprint {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("could not verify fingerprint for router %v", routerId)
|
|
}
|
|
|
|
func (network *Network) RerouteLink(l *model.Link) {
|
|
// This is called from Channel.rxer() and thus may not block
|
|
go func() {
|
|
network.handleRerouteLink(l)
|
|
}()
|
|
}
|
|
|
|
func (network *Network) CreateCircuit(params model.CreateCircuitParams) (*model.Circuit, error) {
|
|
clientId := params.GetClientId()
|
|
service := params.GetServiceId()
|
|
ctx := params.GetLogContext()
|
|
deadline := params.GetDeadline()
|
|
|
|
startTime := time.Now()
|
|
|
|
instanceId, serviceId := parseInstanceIdAndService(service)
|
|
|
|
// 1: Allocate Circuit Identifier
|
|
circuitId := params.GetCircuitId()
|
|
if circuitId == "" {
|
|
var err error
|
|
circuitId, err = idgen.NewUUIDString()
|
|
if err != nil {
|
|
network.CircuitFailedEvent(circuitId, params, startTime, nil, nil, CircuitFailureIdGenerationError)
|
|
return nil, err
|
|
}
|
|
}
|
|
ctx.WithFields(map[string]interface{}{
|
|
"circuitId": circuitId,
|
|
"serviceId": service,
|
|
"attemptNumber": 1,
|
|
})
|
|
logger := pfxlog.ChannelLogger(logcontext.SelectPath).Wire(ctx).Entry
|
|
|
|
circuit := &model.Circuit{
|
|
Id: circuitId,
|
|
ClientId: clientId.Token,
|
|
ServiceId: serviceId,
|
|
Path: &model.Path{}, // empty until the circuit is built
|
|
}
|
|
|
|
// Reserve the circuit ID in the map to prevent collisions and protect routing.
|
|
// On any error path, the deferred cleanup removes the stub.
|
|
if !network.Circuit.Reserve(circuit) {
|
|
return nil, fmt.Errorf("circuit id %v already in use", circuitId)
|
|
}
|
|
removeReserved := true
|
|
defer func() {
|
|
if removeReserved {
|
|
network.Circuit.Remove(circuit)
|
|
}
|
|
}()
|
|
|
|
attempt := uint32(0)
|
|
allCleanups := make(map[string]struct{})
|
|
rs := network.newRouteSender(circuitId)
|
|
defer func() { network.removeRouteSender(rs) }()
|
|
for {
|
|
// 2: Find Service
|
|
svc, err := network.Service.Read(serviceId)
|
|
if err != nil {
|
|
network.CircuitFailedEvent(circuitId, params, startTime, nil, nil, CircuitFailureInvalidService)
|
|
network.ServiceDialOtherError(serviceId)
|
|
return circuit, err
|
|
}
|
|
logger = logger.WithField("serviceName", svc.Name)
|
|
|
|
// 3: select terminator
|
|
strategy, terminator, pathNodes, strategyData, circuitErr := network.selectPath(params, svc, instanceId, ctx)
|
|
if circuitErr != nil {
|
|
network.CircuitFailedEvent(circuitId, params, startTime, nil, nil, circuitErr.Cause())
|
|
network.ServiceDialOtherError(serviceId)
|
|
return circuit, circuitErr
|
|
}
|
|
|
|
circuit.Terminator = terminator
|
|
|
|
// 4: Create Path
|
|
path, pathErr := network.CreatePathWithNodes(pathNodes)
|
|
if pathErr != nil {
|
|
network.CircuitFailedEvent(circuitId, params, startTime, nil, terminator, pathErr.Cause())
|
|
network.ServiceDialOtherError(serviceId)
|
|
return circuit, pathErr
|
|
}
|
|
|
|
circuit.Path = path
|
|
|
|
// get circuit tags
|
|
tags := params.GetCircuitTags(terminator)
|
|
|
|
circuit.Tags = tags
|
|
|
|
// 4a: Create Route Messages
|
|
rms := network.CreateRouteMessages(path, attempt, circuitId, terminator, deadline)
|
|
rms[len(rms)-1].Egress.PeerData = clientId.Data
|
|
for _, msg := range rms {
|
|
msg.Context = &ctrl_pb.Context{
|
|
Fields: ctx.GetStringFields(),
|
|
ChannelMask: ctx.GetChannelsMask(),
|
|
}
|
|
msg.Tags = tags
|
|
}
|
|
|
|
// 5: Routing
|
|
logger.Debug("route attempt for circuit")
|
|
peerData, cleanups, circuitErr := rs.route(attempt, path, rms, strategy, terminator, ctx.Clone())
|
|
for k, v := range cleanups {
|
|
allCleanups[k] = v
|
|
}
|
|
if circuitErr != nil {
|
|
logger.WithError(circuitErr).Warn("route attempt for circuit failed")
|
|
network.CircuitFailedEvent(circuitId, params, startTime, path, terminator, circuitErr.Cause())
|
|
attempt++
|
|
ctx.WithField("attemptNumber", attempt)
|
|
logger = logger.WithField("attemptNumber", attempt)
|
|
if attempt < network.options.CreateCircuitRetries {
|
|
continue
|
|
} else {
|
|
// revert successful routes
|
|
logger.Warnf("circuit creation failed after [%d] attempts, sending cleanup unroutes", network.options.CreateCircuitRetries)
|
|
for cleanupRId := range allCleanups {
|
|
if r := network.GetConnectedRouter(cleanupRId); r != nil {
|
|
if err := sendUnroute(r, circuitId, true); err == nil {
|
|
logger.WithField("routerId", cleanupRId).Debug("sent cleanup unroute for circuit")
|
|
} else {
|
|
logger.WithField("routerId", cleanupRId).Error("error sending cleanup unroute for circuit")
|
|
}
|
|
} else {
|
|
logger.WithField("routerId", cleanupRId).Error("router for circuit cleanup not connected")
|
|
}
|
|
}
|
|
|
|
return circuit, fmt.Errorf("exceeded maximum [%d] retries creating circuit [c/%s] (%w)", network.options.CreateCircuitRetries, circuitId, circuitErr)
|
|
}
|
|
}
|
|
|
|
// 5.a: Unroute Abandoned Routers (from Previous Attempts)
|
|
usedRouters := make(map[string]struct{})
|
|
for _, r := range path.Nodes {
|
|
usedRouters[r.Id] = struct{}{}
|
|
}
|
|
cleanupCount := 0
|
|
for cleanupRId := range allCleanups {
|
|
if _, found := usedRouters[cleanupRId]; !found {
|
|
cleanupCount++
|
|
if r := network.GetConnectedRouter(cleanupRId); r != nil {
|
|
if err := sendUnroute(r, circuitId, true); err == nil {
|
|
logger.WithField("routerId", cleanupRId).Debug("sent abandoned cleanup unroute for circuit to router")
|
|
} else {
|
|
logger.WithField("routerId", cleanupRId).WithError(err).Error("error sending abandoned cleanup unroute for circuit to router")
|
|
}
|
|
} else {
|
|
logger.WithField("routerId", cleanupRId).Error("router not connected for circuit, abandoned cleanup")
|
|
}
|
|
}
|
|
}
|
|
logger.Debugf("cleaned up [%d] abandoned routers for circuit", cleanupCount)
|
|
|
|
path.InitiatorLocalAddr = string(clientId.Data[uint32(ctrl_msg.InitiatorLocalAddressHeader)])
|
|
path.InitiatorRemoteAddr = string(clientId.Data[uint32(ctrl_msg.InitiatorRemoteAddressHeader)])
|
|
path.TerminatorLocalAddr = string(peerData[uint32(ctrl_msg.TerminatorLocalAddressHeader)])
|
|
path.TerminatorRemoteAddr = string(peerData[uint32(ctrl_msg.TerminatorRemoteAddressHeader)])
|
|
|
|
delete(peerData, uint32(ctrl_msg.InitiatorLocalAddressHeader))
|
|
delete(peerData, uint32(ctrl_msg.InitiatorRemoteAddressHeader))
|
|
delete(peerData, uint32(ctrl_msg.TerminatorLocalAddressHeader))
|
|
delete(peerData, uint32(ctrl_msg.TerminatorRemoteAddressHeader))
|
|
|
|
for k, v := range strategyData {
|
|
peerData[k] = v
|
|
}
|
|
|
|
now := time.Now()
|
|
// 6: Create Circuit Object
|
|
circuit.PeerData = peerData
|
|
circuit.CreatedAt = now
|
|
circuit.UpdatedAt = now
|
|
|
|
removeReserved = false // circuit is finalized, don't remove on defer
|
|
creationTimespan := time.Since(startTime)
|
|
network.CircuitEvent(event.CircuitCreated, circuit, &creationTimespan)
|
|
strategy.NotifyEvent(xt.NewDialSucceeded(terminator))
|
|
|
|
logger.WithField("path", circuit.Path).
|
|
WithField("terminator_local_address", circuit.Path.TerminatorLocalAddr).
|
|
WithField("terminator_remote_address", circuit.Path.TerminatorRemoteAddr).
|
|
Debug("created circuit")
|
|
return circuit, nil
|
|
}
|
|
}
|
|
|
|
func (network *Network) ReportForwardingFaults(ffr *ForwardingFaultReport) {
|
|
go network.handleForwardingFaults(ffr)
|
|
}
|
|
|
|
func parseInstanceIdAndService(service string) (string, string) {
|
|
atIndex := strings.IndexRune(service, '@')
|
|
if atIndex < 0 {
|
|
return "", service
|
|
}
|
|
identityId := service[0:atIndex]
|
|
serviceId := service[atIndex+1:]
|
|
return identityId, serviceId
|
|
}
|
|
|
|
func (network *Network) selectPath(params model.CreateCircuitParams, svc *model.Service, instanceId string, ctx logcontext.Context) (xt.Strategy, xt.CostedTerminator, []*model.Router, xt.PeerData, CircuitError) {
|
|
paths := map[string]*PathAndCost{}
|
|
var weightedTerminators []xt.CostedTerminator
|
|
var errList []error
|
|
|
|
log := pfxlog.ChannelLogger(logcontext.SelectPath).Wire(ctx)
|
|
|
|
hasOfflineRouters := false
|
|
pathError := false
|
|
|
|
for _, terminator := range svc.Terminators {
|
|
if terminator.InstanceId != instanceId {
|
|
continue
|
|
}
|
|
|
|
pathAndCost, found := paths[terminator.Router]
|
|
if !found {
|
|
dstR := network.Router.GetConnected(terminator.GetRouterId())
|
|
if dstR == nil {
|
|
err := fmt.Errorf("router with id=%v on terminator with id=%v for service name=%v is not online",
|
|
terminator.GetRouterId(), terminator.GetId(), svc.Name)
|
|
log.Debugf("error while calculating path for service %v: %v", svc.Id, err)
|
|
|
|
errList = append(errList, err)
|
|
hasOfflineRouters = true
|
|
continue
|
|
}
|
|
|
|
path, cost, err := network.shortestPath(params.GetSourceRouter(), dstR)
|
|
if err != nil {
|
|
log.Debugf("error while calculating path for service %v: %v", svc.Id, err)
|
|
errList = append(errList, err)
|
|
pathError = true
|
|
continue
|
|
}
|
|
|
|
pathAndCost = newPathAndCost(path, cost)
|
|
paths[terminator.GetRouterId()] = pathAndCost
|
|
}
|
|
|
|
dynamicCost := xt.GlobalCosts().GetDynamicCost(terminator.Id)
|
|
unbiasedCost := uint32(terminator.Cost) + uint32(dynamicCost) + pathAndCost.cost
|
|
biasedCost := terminator.Precedence.GetBiasedCost(unbiasedCost)
|
|
costedTerminator := &model.RoutingTerminator{
|
|
Terminator: terminator,
|
|
RouteCost: biasedCost,
|
|
}
|
|
weightedTerminators = append(weightedTerminators, costedTerminator)
|
|
}
|
|
|
|
if len(svc.Terminators) == 0 {
|
|
return nil, nil, nil, nil, newCircuitErrorf(CircuitFailureNoTerminators, "service %v has no terminators", svc.Id)
|
|
}
|
|
|
|
if len(weightedTerminators) == 0 {
|
|
if pathError {
|
|
return nil, nil, nil, nil, newCircuitErrWrap(CircuitFailureNoPath, errors.Join(errList...))
|
|
}
|
|
|
|
if hasOfflineRouters {
|
|
return nil, nil, nil, nil, newCircuitErrorf(CircuitFailureNoOnlineTerminators, "service %v has no online terminators for instanceId %v", svc.Id, instanceId)
|
|
}
|
|
|
|
return nil, nil, nil, nil, newCircuitErrorf(CircuitFailureNoTerminators, "service %v has no terminators for instanceId %v", svc.Id, instanceId)
|
|
}
|
|
|
|
strategy, err := network.strategyRegistry.GetStrategy(svc.TerminatorStrategy)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, newCircuitErrWrap(CircuitFailureInvalidStrategy, err)
|
|
}
|
|
|
|
sort.Slice(weightedTerminators, func(i, j int) bool {
|
|
return weightedTerminators[i].GetRouteCost() < weightedTerminators[j].GetRouteCost()
|
|
})
|
|
|
|
terminator, peerData, err := strategy.Select(params, weightedTerminators)
|
|
|
|
if err != nil {
|
|
return nil, nil, nil, nil, newCircuitErrorf(CircuitFailureStrategyError, "strategy %v errored selecting terminator for service %v: %v", svc.TerminatorStrategy, svc.Id, err)
|
|
}
|
|
|
|
if terminator == nil {
|
|
return nil, nil, nil, nil, newCircuitErrorf(CircuitFailureStrategyError, "strategy %v did not select terminator for service %v", svc.TerminatorStrategy, svc.Id)
|
|
}
|
|
|
|
path := paths[terminator.GetRouterId()].path
|
|
|
|
if log.Logger.IsLevelEnabled(logrus.DebugLevel) {
|
|
buf := strings.Builder{}
|
|
buf.WriteString("[")
|
|
if len(weightedTerminators) > 0 {
|
|
fmt.Fprintf(&buf, "%v: %v", weightedTerminators[0].GetId(), weightedTerminators[0].GetRouteCost())
|
|
for _, t := range weightedTerminators[1:] {
|
|
buf.WriteString(", ")
|
|
fmt.Fprintf(&buf, "%v: %v", t.GetId(), t.GetRouteCost())
|
|
}
|
|
}
|
|
buf.WriteString("]")
|
|
var routerIds []string
|
|
for _, r := range path {
|
|
routerIds = append(routerIds, fmt.Sprintf("r/%s", r.Id))
|
|
}
|
|
pathStr := strings.Join(routerIds, "->")
|
|
log.Debugf("selected terminator %v for path %v from %v", terminator.GetId(), pathStr, buf.String())
|
|
}
|
|
|
|
return strategy, terminator, path, peerData, nil
|
|
}
|
|
|
|
func (network *Network) RemoveCircuit(circuitId string, now bool) error {
|
|
log := pfxlog.Logger().WithField("circuitId", circuitId)
|
|
|
|
if circuit, found := network.Circuit.Get(circuitId); found {
|
|
for _, r := range circuit.Path.Nodes {
|
|
err := sendUnroute(r, circuit.Id, now)
|
|
if err != nil {
|
|
log.Errorf("error sending unroute to [r/%s] (%s)", r.Id, err)
|
|
}
|
|
}
|
|
|
|
network.Circuit.Remove(circuit)
|
|
network.CircuitEvent(event.CircuitDeleted, circuit, nil)
|
|
|
|
if svc, err := network.Service.Read(circuit.ServiceId); err == nil {
|
|
if strategy, err := network.strategyRegistry.GetStrategy(svc.TerminatorStrategy); strategy != nil {
|
|
strategy.NotifyEvent(xt.NewCircuitRemoved(circuit.Terminator))
|
|
} else if err != nil {
|
|
log.WithError(err).WithField("terminatorStrategy", svc.TerminatorStrategy).Warn("failed to notify strategy of circuit end, invalid strategy")
|
|
}
|
|
} else {
|
|
log.WithError(err).Error("unable to get service for circuit")
|
|
}
|
|
|
|
log.Debug("removed circuit")
|
|
|
|
return nil
|
|
}
|
|
return InvalidCircuitError{circuitId: circuitId}
|
|
}
|
|
|
|
func (network *Network) CreatePath(srcR, dstR *model.Router) (*model.Path, error) {
|
|
ingressId, err := idgen.NewUUIDString()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
egressId, err := idgen.NewUUIDString()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
path := &model.Path{
|
|
Links: make([]*model.Link, 0),
|
|
IngressId: ingressId,
|
|
EgressId: egressId,
|
|
Nodes: make([]*model.Router, 0),
|
|
}
|
|
path.Nodes = append(path.Nodes, srcR)
|
|
path.Nodes = append(path.Nodes, dstR)
|
|
|
|
return network.UpdatePath(path)
|
|
}
|
|
|
|
func (network *Network) setLinks(path *model.Path) error {
|
|
if len(path.Nodes) > 1 {
|
|
for i := 0; i < len(path.Nodes)-1; i++ {
|
|
if link, found := network.Link.LeastExpensiveLink(path.Nodes[i], path.Nodes[i+1]); found {
|
|
path.Links = append(path.Links, link)
|
|
} else {
|
|
return fmt.Errorf("no link from r/%v to r/%v", path.Nodes[i].Id, path.Nodes[i+1].Id)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (network *Network) AddRouterPresenceHandler(h model.RouterPresenceHandler) {
|
|
network.routerPresenceHandlers.Append(h)
|
|
}
|
|
|
|
func (network *Network) Run() {
|
|
defer logrus.Info("exited")
|
|
logrus.Info("started")
|
|
|
|
go network.watchdog()
|
|
|
|
ticker := time.NewTicker(time.Duration(network.options.CycleSeconds) * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
network.clean()
|
|
network.smart()
|
|
network.Link.ScanForDeadLinks()
|
|
|
|
case <-network.closeNotify:
|
|
network.eventDispatcher.RemoveMetricsMessageHandler(network)
|
|
network.metricsRegistry.DisposeAll()
|
|
return
|
|
}
|
|
|
|
// notify the watchdog that we're processing
|
|
select {
|
|
case network.watchdogCh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (network *Network) watchdog() {
|
|
watchdogInterval := 2 * time.Duration(network.options.CycleSeconds) * time.Second
|
|
consecutiveFails := 0
|
|
|
|
watchDogTicker := time.NewTicker(watchdogInterval)
|
|
defer watchDogTicker.Stop()
|
|
|
|
for {
|
|
// check every 2x cycle seconds
|
|
select {
|
|
case <-watchDogTicker.C:
|
|
case <-network.closeNotify:
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-network.watchdogCh:
|
|
consecutiveFails = 0
|
|
continue
|
|
case <-network.closeNotify:
|
|
return
|
|
default:
|
|
consecutiveFails++
|
|
// network.Run didn't complete, something is stalling it
|
|
pfxlog.Logger().
|
|
WithField("watchdogInterval", watchdogInterval.String()).
|
|
WithField("consecutiveFails", consecutiveFails).
|
|
Warn("network.Run did not finish within watchdog interval")
|
|
|
|
if consecutiveFails == 3 {
|
|
debugz.DumpStack()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (network *Network) handleRerouteLink(l *model.Link) {
|
|
log := logrus.WithField("linkId", l.Id)
|
|
log.Info("changed link")
|
|
if err := network.rerouteLink(l, time.Now().Add(config.DefaultOptionsRouteTimeout)); err != nil {
|
|
log.WithError(err).Error("unexpected error rerouting link")
|
|
}
|
|
}
|
|
|
|
func (network *Network) handleForwardingFaults(ffr *ForwardingFaultReport) {
|
|
network.fault(ffr)
|
|
}
|
|
|
|
func (network *Network) AddCapability(capability string) {
|
|
network.lock.Lock()
|
|
defer network.lock.Unlock()
|
|
network.capabilities = append(network.capabilities, capability)
|
|
}
|
|
|
|
func (network *Network) GetCapabilities() []string {
|
|
network.lock.Lock()
|
|
defer network.lock.Unlock()
|
|
return network.capabilities
|
|
}
|
|
|
|
func (network *Network) RemoveLink(linkId string) {
|
|
log := pfxlog.Logger().WithField("linkId", linkId)
|
|
|
|
link, _ := network.Link.Get(linkId)
|
|
var iteration uint32
|
|
|
|
var routerList []*model.Router
|
|
if link != nil {
|
|
iteration = link.Iteration
|
|
routerList = []*model.Router{link.Src}
|
|
if dst := link.GetDest(); dst != nil {
|
|
routerList = append(routerList, dst)
|
|
}
|
|
log = log.WithField("srcRouterId", link.Src.Id).
|
|
WithField("dstRouterId", link.DstId).
|
|
WithField("iteration", iteration)
|
|
log.Info("deleting known link")
|
|
} else {
|
|
routerList = network.AllConnectedRouters()
|
|
log.Info("deleting unknown link (sending link fault to all connected routers)")
|
|
}
|
|
|
|
for _, router := range routerList {
|
|
fault := &ctrl_pb.Fault{
|
|
Subject: ctrl_pb.FaultSubject_LinkFault,
|
|
Id: linkId,
|
|
Iteration: iteration,
|
|
}
|
|
|
|
if ctrl := router.Control; ctrl != nil {
|
|
if err := protobufs.MarshalTyped(fault).WithTimeout(15 * time.Second).Send(ctrl.GetDefaultSender()); err != nil {
|
|
log.WithField("faultDestRouterId", router.Id).WithError(err).
|
|
Error("failed to send link fault to router on link removal")
|
|
} else {
|
|
log.WithField("faultDestRouterId", router.Id).WithError(err).
|
|
Info("sent link fault to router on link removal")
|
|
}
|
|
}
|
|
}
|
|
|
|
if link != nil {
|
|
network.Link.Remove(link)
|
|
network.RerouteLink(link)
|
|
}
|
|
}
|
|
|
|
func (network *Network) rerouteLink(l *model.Link, deadline time.Time) error {
|
|
circuits := network.Circuit.All()
|
|
for _, circuit := range circuits {
|
|
if circuit.Path.UsesLink(l) {
|
|
log := logrus.WithField("linkId", l.Id).
|
|
WithField("circuitId", circuit.Id)
|
|
log.Info("circuit uses link")
|
|
if err := network.rerouteCircuit(circuit, deadline); err != nil {
|
|
log.WithError(err).Error("error rerouting circuit, removing")
|
|
if err := network.RemoveCircuit(circuit.Id, true); err != nil {
|
|
log.WithError(err).Error("error removing circuit after reroute failure")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (network *Network) rerouteCircuitWithTries(circuit *model.Circuit, retries int) bool {
|
|
log := pfxlog.Logger().WithField("circuitId", circuit.Id)
|
|
|
|
// Path is nil for reserved circuits that haven't been built yet
|
|
if circuit.Path.IsValid() {
|
|
for i := 0; i < retries; i++ {
|
|
deadline := time.Now().Add(config.DefaultOptionsRouteTimeout)
|
|
err := network.rerouteCircuit(circuit, deadline)
|
|
if err == nil {
|
|
return true
|
|
}
|
|
|
|
log.WithError(err).WithField("attempt", i).Error("error re-routing circuit")
|
|
}
|
|
}
|
|
|
|
if err := network.RemoveCircuit(circuit.Id, true); err != nil {
|
|
log.WithError(err).Error("failure while removing circuit after failed re-route attempt")
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (network *Network) rerouteCircuit(circuit *model.Circuit, deadline time.Time) error {
|
|
log := pfxlog.Logger().WithField("circuitId", circuit.Id)
|
|
if circuit.Rerouting.CompareAndSwap(false, true) {
|
|
defer circuit.Rerouting.Store(false)
|
|
|
|
log.Warn("rerouting circuit")
|
|
|
|
oldPath := circuit.Path
|
|
if cq, err := network.UpdatePath(circuit.Path); err == nil {
|
|
circuit.Path = cq
|
|
circuit.UpdatedAt = time.Now()
|
|
|
|
rms := network.CreateRouteMessages(cq, SmartRerouteAttempt, circuit.Id, circuit.Terminator, deadline)
|
|
|
|
for i := 0; i < len(cq.Nodes); i++ {
|
|
if _, err := sendRoute(cq.Nodes[i], rms[i], network.options.RouteTimeout); err != nil {
|
|
log.WithError(err).Errorf("error sending route to [r/%s]", cq.Nodes[i].Id)
|
|
return err
|
|
}
|
|
}
|
|
|
|
network.unrouteRemovedPathNodes(log, circuit.Id, oldPath, cq)
|
|
|
|
log.Info("rerouted circuit")
|
|
|
|
network.CircuitEvent(event.CircuitUpdated, circuit, nil)
|
|
return nil
|
|
} else {
|
|
return err
|
|
}
|
|
} else {
|
|
log.Info("not rerouting circuit, already in progress")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (network *Network) smartReroute(circuit *model.Circuit, cq *model.Path, deadline time.Time) bool {
|
|
retry := false
|
|
log := pfxlog.Logger().WithField("circuitId", circuit.Id)
|
|
if circuit.Rerouting.CompareAndSwap(false, true) {
|
|
defer circuit.Rerouting.Store(false)
|
|
|
|
oldPath := circuit.Path
|
|
circuit.Path = cq
|
|
circuit.UpdatedAt = time.Now()
|
|
|
|
rms := network.CreateRouteMessages(cq, SmartRerouteAttempt, circuit.Id, circuit.Terminator, deadline)
|
|
|
|
for i := 0; i < len(cq.Nodes); i++ {
|
|
if _, err := sendRoute(cq.Nodes[i], rms[i], network.options.RouteTimeout); err != nil {
|
|
retry = true
|
|
log.WithField("routerId", cq.Nodes[i].Id).WithError(err).Error("error sending smart route update to router")
|
|
break
|
|
}
|
|
}
|
|
|
|
if !retry {
|
|
network.unrouteRemovedPathNodes(log, circuit.Id, oldPath, cq)
|
|
logrus.Debug("rerouted circuit")
|
|
network.CircuitEvent(event.CircuitUpdated, circuit, nil)
|
|
}
|
|
}
|
|
return retry
|
|
}
|
|
|
|
func (network *Network) AcceptMetricsMsg(metrics *metrics_pb.MetricsMessage) {
|
|
if metrics.SourceId == network.nodeId {
|
|
return // ignore metrics coming from the controller itself
|
|
}
|
|
|
|
log := pfxlog.Logger()
|
|
|
|
router, err := network.Router.Read(metrics.SourceId)
|
|
if err != nil {
|
|
log.Debugf("could not find router [r/%s] while processing metrics", metrics.SourceId)
|
|
return
|
|
}
|
|
|
|
for _, link := range network.GetAllLinksForRouter(router.Id) {
|
|
metricId := "link." + link.Id + ".latency"
|
|
var latencyCost int64
|
|
var found bool
|
|
if latency, ok := metrics.Histograms[metricId]; ok {
|
|
latencyCost = int64(latency.Mean)
|
|
found = true
|
|
|
|
metricId = "link." + link.Id + ".queue_time"
|
|
if queueTime, ok := metrics.Histograms[metricId]; ok {
|
|
latencyCost += int64(queueTime.Mean)
|
|
}
|
|
}
|
|
|
|
if found {
|
|
if link.Src.Id == router.Id {
|
|
link.SetSrcLatency(latencyCost) // latency is in nanoseconds
|
|
} else if link.DstId == router.Id {
|
|
link.SetDstLatency(latencyCost) // latency is in nanoseconds
|
|
} else {
|
|
log.Warnf("link not for router")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func sendRoute(r *model.Router, createMsg *ctrl_pb.Route, timeout time.Duration) (xt.PeerData, error) {
|
|
log := pfxlog.Logger().WithField("routerId", r.Id).
|
|
WithField("circuitId", createMsg.CircuitId)
|
|
|
|
log.Debug("sending create route message")
|
|
|
|
msg, err := protobufs.MarshalTyped(createMsg).WithTimeout(timeout).SendForReply(r.Control.GetHighPrioritySender())
|
|
if err != nil {
|
|
log.WithError(err).WithField("timeout", timeout).Error("error sending route message")
|
|
return nil, err
|
|
}
|
|
|
|
if msg.ContentType == ctrl_msg.RouteResultType {
|
|
_, success := msg.Headers[ctrl_msg.RouteResultSuccessHeader]
|
|
if !success {
|
|
message := "route error, but no error message from router"
|
|
if errMsg, found := msg.Headers[ctrl_msg.RouteResultErrorHeader]; found {
|
|
message = string(errMsg)
|
|
}
|
|
return nil, errors.New(message)
|
|
}
|
|
|
|
peerData := xt.PeerData{}
|
|
for k, v := range msg.Headers {
|
|
if k > 0 {
|
|
peerData[uint32(k)] = v
|
|
}
|
|
}
|
|
|
|
return peerData, nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected response type %v received in reply to route request", msg.ContentType)
|
|
}
|
|
|
|
func sendUnroute(r *model.Router, circuitId string, now bool) error {
|
|
unroute := &ctrl_pb.Unroute{
|
|
CircuitId: circuitId,
|
|
Now: now,
|
|
}
|
|
return protobufs.MarshalTyped(unroute).Send(r.Control.GetHighPrioritySender())
|
|
}
|
|
|
|
// unrouteRemovedPathNodes sends Unroute to any nodes that were in the old path but are not in the new path.
|
|
// This handles the case where a circuit reroute changes intermediate transit nodes.
|
|
func (network *Network) unrouteRemovedPathNodes(log *logrus.Entry, circuitId string, oldPath, newPath *model.Path) {
|
|
newNodeIds := make(map[string]struct{}, len(newPath.Nodes))
|
|
for _, r := range newPath.Nodes {
|
|
newNodeIds[r.Id] = struct{}{}
|
|
}
|
|
|
|
for _, r := range oldPath.Nodes {
|
|
if _, ok := newNodeIds[r.Id]; !ok {
|
|
if cr := network.GetConnectedRouter(r.Id); cr != nil {
|
|
if err := sendUnroute(cr, circuitId, true); err != nil {
|
|
log.WithError(err).Errorf("error sending unroute to removed path node [r/%s]", r.Id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (network *Network) showOptions() {
|
|
if jsonOptions, err := json.MarshalIndent(network.options, "", " "); err == nil {
|
|
pfxlog.Logger().Infof("network = %s", string(jsonOptions))
|
|
} else {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
type renderConfig interface {
|
|
RenderJsonConfig() (string, error)
|
|
}
|
|
|
|
func (network *Network) routerDeleted(routerId string) {
|
|
circuits := network.GetAllCircuits()
|
|
for _, circuit := range circuits {
|
|
if circuit.HasRouter(routerId) {
|
|
path := circuit.Path
|
|
|
|
// If we're either the initiator, terminator (or both), cleanup the circuit since
|
|
// we won't be able to re-establish it, and we'll never get a circuit fault
|
|
if path.Nodes[0].Id == routerId || path.Nodes[len(path.Nodes)-1].Id == routerId {
|
|
if err := network.RemoveCircuit(circuit.Id, true); err != nil {
|
|
pfxlog.Logger().WithField("routerId", routerId).
|
|
WithField("circuitId", circuit.Id).
|
|
WithError(err).Error("unable to remove circuit after router was deleted")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var DbSnapshotTooFrequentError = dbSnapshotTooFrequentError{}
|
|
|
|
type dbSnapshotTooFrequentError struct{}
|
|
|
|
func (d dbSnapshotTooFrequentError) Error() string {
|
|
return "may snapshot database at most once per minute"
|
|
}
|
|
|
|
func (network *Network) SnapshotDatabase() error {
|
|
_, err := network.SnapshotDatabaseToFile("")
|
|
return err
|
|
}
|
|
|
|
func (network *Network) SnapshotDatabaseToFile(path string) (string, error) {
|
|
network.lock.Lock()
|
|
defer network.lock.Unlock()
|
|
|
|
if network.lastSnapshot.Add(time.Minute).After(time.Now()) {
|
|
return "", DbSnapshotTooFrequentError
|
|
}
|
|
|
|
actualPath := path
|
|
if actualPath == "" {
|
|
actualPath = "__DB_DIR__/__DB_FILE__-__DATE__-__TIME__"
|
|
}
|
|
|
|
err := network.GetDb().View(func(tx *bbolt.Tx) error {
|
|
currentIndex := db.LoadCurrentRaftIndex(tx)
|
|
actualPath = strings.ReplaceAll(actualPath, "__RAFT_INDEX__", fmt.Sprintf("%v", currentIndex))
|
|
actualPath = strings.ReplaceAll(actualPath, "RAFT_INDEX", fmt.Sprintf("%v", currentIndex))
|
|
|
|
var err error
|
|
actualPath, _, err = network.GetDb().SnapshotInTx(tx, actualPath)
|
|
return err
|
|
})
|
|
|
|
if err == nil {
|
|
network.lastSnapshot = time.Now()
|
|
}
|
|
|
|
return actualPath, err
|
|
}
|
|
|
|
func (network *Network) RestoreSnapshot(cmd *command.SyncSnapshotCommand, index uint64) error {
|
|
log := pfxlog.Logger()
|
|
currentTimelineId, err := network.GetDb().GetTimelineId(boltz.TimelineModeDefault, shortid.Generate)
|
|
if err != nil {
|
|
log.WithError(err).Error("unable to get current timeline id")
|
|
}
|
|
if currentTimelineId != "" && currentTimelineId == cmd.TimelineId {
|
|
log.WithField("timelineId", cmd.TimelineId).Info("snapshot already current, skipping reload")
|
|
// DB already restored; ensure cluster id then raft index (index last, see main path).
|
|
if err = network.ensureClusterId(cmd.ClusterId); err != nil {
|
|
return fmt.Errorf("failed to set cluster id for already-current snapshot (%w)", err)
|
|
}
|
|
if err = network.ensureRaftIndex(index); err != nil {
|
|
return fmt.Errorf("failed to set raft index for already-current snapshot (%w)", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
buf := bytes.NewBuffer(cmd.Snapshot)
|
|
reader, err := gzip.NewReader(buf)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create gz reader for reading migration snapshot during restore (%w)", err)
|
|
}
|
|
|
|
network.GetDb().RestoreFromReader(reader)
|
|
|
|
// Write the cluster id before the raft index. The index is the completion gate on restart (the
|
|
// FSM skips entries at or below the stored index), so persist it last: any earlier failure then
|
|
// replays and retries instead of skipping the command with a blank cluster id.
|
|
if err = network.ensureClusterId(cmd.ClusterId); err != nil {
|
|
return fmt.Errorf("failed to set cluster id after db restore (%w)", err)
|
|
}
|
|
if err = network.ensureRaftIndex(index); err != nil {
|
|
return fmt.Errorf("failed to set raft index after db restore (%w)", err)
|
|
}
|
|
|
|
time.AfterFunc(5*time.Second, func() {
|
|
log.Info("database restore requires controller restart. exiting...")
|
|
os.Exit(0)
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// ensureClusterId writes the cluster id if one is not already set (no-op when empty or matching).
|
|
// The snapshot restore replaces the whole db, which carries no cluster id, so it must be set here.
|
|
func (network *Network) ensureClusterId(clusterId string) error {
|
|
if clusterId == "" {
|
|
return nil
|
|
}
|
|
_, err := db.InitClusterId(network.GetDb(), nil, clusterId)
|
|
return err
|
|
}
|
|
|
|
// ensureRaftIndex records the raft index if the stored one is behind it, returning an error on
|
|
// failure so a missed index update surfaces rather than being treated as success.
|
|
func (network *Network) ensureRaftIndex(index uint64) error {
|
|
return network.GetDb().Update(nil, func(ctx boltz.MutateContext) error {
|
|
if db.LoadCurrentRaftIndex(ctx.Tx()) >= index {
|
|
return nil
|
|
}
|
|
raftBucket := boltz.GetOrCreatePath(ctx.Tx(), db.RootBucket, db.MetadataBucket)
|
|
raftBucket.SetInt64(db.FieldRaftIndex, int64(index), nil)
|
|
return raftBucket.GetError()
|
|
})
|
|
}
|
|
|
|
func (network *Network) AddInspectTarget(target InspectTarget) {
|
|
network.inspectionTargets.Append(target)
|
|
}
|
|
|
|
// SetCtrlDialerValidator registers the ctrl dialer's validation function with the network.
|
|
func (network *Network) SetCtrlDialerValidator(validator CtrlDialerValidator) {
|
|
network.ctrlDialerValidator = validator
|
|
}
|
|
|
|
// GetCtrlDialerValidator returns the registered ctrl dialer validation function, or nil.
|
|
func (network *Network) GetCtrlDialerValidator() CtrlDialerValidator {
|
|
return network.ctrlDialerValidator
|
|
}
|
|
|
|
func (network *Network) ValidateRouterLinks(router *model.Router, cb LinkValidationCallback) {
|
|
request := &ctrl_pb.InspectRequest{RequestedValues: []string{"links"}}
|
|
resp := &ctrl_pb.InspectResponse{}
|
|
respMsg, err := protobufs.MarshalTyped(request).WithTimeout(time.Minute).SendForReply(router.Control.GetDefaultSender())
|
|
if err = protobufs.TypedResponse(resp).Unmarshall(respMsg, err); err != nil {
|
|
network.reportRouterLinksError(router, err, cb)
|
|
return
|
|
}
|
|
|
|
var linkDetails *inspect.LinksInspectResult
|
|
for _, val := range resp.Values {
|
|
if val.Name == "links" {
|
|
if err = json.Unmarshal([]byte(val.Value), &linkDetails); err != nil {
|
|
network.reportRouterLinksError(router, err, cb)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if linkDetails == nil {
|
|
if len(resp.Errors) > 0 {
|
|
err = errors.New(strings.Join(resp.Errors, ","))
|
|
network.reportRouterLinksError(router, err, cb)
|
|
return
|
|
}
|
|
network.reportRouterLinksError(router, errors.New("no link details returned from router"), cb)
|
|
return
|
|
}
|
|
|
|
linkMap := network.Link.GetLinkMap()
|
|
|
|
result := &mgmt_pb.RouterLinkDetails{
|
|
RouterId: router.Id,
|
|
RouterName: router.Name,
|
|
ValidateSuccess: true,
|
|
}
|
|
|
|
for _, link := range linkDetails.Links {
|
|
detail := &mgmt_pb.RouterLinkDetail{
|
|
LinkId: link.Id,
|
|
RouterState: mgmt_pb.LinkState_LinkEstablished,
|
|
DestRouterId: link.Dest,
|
|
Dialed: link.Dialed,
|
|
}
|
|
detail.DestConnected = network.ConnectedRouter(link.Dest)
|
|
if ctrlLink, found := linkMap[link.Id]; found {
|
|
detail.CtrlState = mgmt_pb.LinkState_LinkEstablished
|
|
detail.IsValid = detail.DestConnected
|
|
if link.Dialed { // only compare against dialer side of the link, as src/dst will be flipped on the listener side
|
|
network.checkLinkConns(ctrlLink, link, detail)
|
|
}
|
|
} else {
|
|
detail.CtrlState = mgmt_pb.LinkState_LinkUnknown
|
|
detail.IsValid = !detail.DestConnected
|
|
}
|
|
delete(linkMap, link.Id)
|
|
result.LinkDetails = append(result.LinkDetails, detail)
|
|
}
|
|
|
|
for _, link := range linkMap {
|
|
related := false
|
|
dest := ""
|
|
if link.Src.Id == router.Id {
|
|
related = true
|
|
dest = link.DstId
|
|
} else if link.DstId == router.Id {
|
|
related = true
|
|
dest = link.Src.Id
|
|
}
|
|
|
|
if related {
|
|
detail := &mgmt_pb.RouterLinkDetail{
|
|
LinkId: link.Id,
|
|
CtrlState: mgmt_pb.LinkState_LinkEstablished,
|
|
DestConnected: network.ConnectedRouter(dest),
|
|
RouterState: mgmt_pb.LinkState_LinkUnknown,
|
|
IsValid: false,
|
|
DestRouterId: dest,
|
|
Dialed: link.Src.Id == router.Id,
|
|
}
|
|
result.LinkDetails = append(result.LinkDetails, detail)
|
|
}
|
|
}
|
|
|
|
cb(result)
|
|
}
|
|
|
|
func (network *Network) checkLinkConns(ctrlLink *model.Link, routerLink *inspect.LinkInspectDetail, result *mgmt_pb.RouterLinkDetail) {
|
|
sortF := func(v []*ctrl_pb.LinkConn) []*ctrl_pb.LinkConn {
|
|
return slices.SortedFunc(slices.Values(v), func(e *ctrl_pb.LinkConn, e2 *ctrl_pb.LinkConn) int {
|
|
if diff := strings.Compare(e.Type, e2.Type); diff != 0 {
|
|
return diff
|
|
}
|
|
if diff := strings.Compare(e.LocalAddr, e2.LocalAddr); diff != 0 {
|
|
return diff
|
|
}
|
|
return strings.Compare(e.RemoteAddr, e2.RemoteAddr)
|
|
})
|
|
}
|
|
|
|
var routerConns []*ctrl_pb.LinkConn
|
|
|
|
for _, v := range routerLink.Connections {
|
|
routerConns = append(routerConns, &ctrl_pb.LinkConn{
|
|
Type: v.Type,
|
|
LocalAddr: v.Source,
|
|
RemoteAddr: v.Dest,
|
|
})
|
|
}
|
|
|
|
// The version comes from the hello, so only the connected instance has it. Not knowing it means the
|
|
// comparison cannot be made, which is not a fault of the link.
|
|
if srcR := ctrlLink.Src; srcR != nil {
|
|
connectedSrc := network.Router.GetConnected(srcR.Id)
|
|
if connectedSrc == nil || connectedSrc.VersionInfo == nil {
|
|
return
|
|
}
|
|
hasMinVersion, err := connectedSrc.VersionInfo.HasMinimumVersion("v1.6.6")
|
|
if err != nil {
|
|
result.IsValid = false
|
|
result.Messages = append(result.Messages, err.Error())
|
|
return
|
|
}
|
|
if !hasMinVersion {
|
|
return
|
|
}
|
|
}
|
|
|
|
ctrlConnState := ctrlLink.GetConnsState()
|
|
ctrlConns := sortF(ctrlConnState.GetConns())
|
|
routerConns = sortF(routerConns)
|
|
|
|
if len(ctrlConns) != len(routerConns) {
|
|
result.Messages = append(result.Messages, fmt.Sprintf("for link %s, len(ctrlConns): %d != len(routerConns): %d",
|
|
ctrlLink.Id, len(ctrlConns), len(ctrlConns)))
|
|
return
|
|
}
|
|
|
|
if routerLink.ConnStateIteration != ctrlConnState.GetStateIteration() {
|
|
result.Messages = append(result.Messages, fmt.Sprintf("for link %s, conn state iteration doesn't match, ctrl: %d != router: %d",
|
|
ctrlLink.Id, ctrlConnState.GetStateIteration(), routerLink.ConnStateIteration))
|
|
return
|
|
}
|
|
|
|
for i := 0; i < len(ctrlConns); i++ {
|
|
ctrlConn := ctrlConns[i]
|
|
routerConn := routerConns[i]
|
|
if ctrlConn.Type != routerConn.Type {
|
|
result.IsValid = false
|
|
result.Messages = append(result.Messages, fmt.Sprintf("for link %s, type doesn't match. ctrl %s != router %s",
|
|
ctrlLink.Id, ctrlConn.Type, routerConn.Type))
|
|
}
|
|
if ctrlConn.LocalAddr != routerConn.LocalAddr {
|
|
result.IsValid = false
|
|
result.Messages = append(result.Messages, fmt.Sprintf("for link %s, local addr doesn't match. ctrl %s != router %s",
|
|
ctrlLink.Id, ctrlConn.LocalAddr, routerConn.LocalAddr))
|
|
}
|
|
if ctrlConn.RemoteAddr != routerConn.RemoteAddr {
|
|
result.IsValid = false
|
|
result.Messages = append(result.Messages, fmt.Sprintf("for link %s, remote addr doesn't match. ctrl %s != router %s",
|
|
ctrlLink.Id, ctrlConn.RemoteAddr, routerConn.RemoteAddr))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (network *Network) reportRouterLinksError(router *model.Router, err error, cb LinkValidationCallback) {
|
|
result := &mgmt_pb.RouterLinkDetails{
|
|
RouterId: router.Id,
|
|
RouterName: router.Name,
|
|
ValidateSuccess: false,
|
|
Message: err.Error(),
|
|
}
|
|
cb(result)
|
|
}
|
|
|
|
func minCost(q map[*model.Router]bool, dist map[*model.Router]int64) *model.Router {
|
|
if len(dist) < 1 {
|
|
return nil
|
|
}
|
|
|
|
currentMin := int64(math.MaxInt64)
|
|
var selected *model.Router
|
|
for r := range q {
|
|
d := dist[r]
|
|
if d <= currentMin {
|
|
selected = r
|
|
currentMin = d
|
|
}
|
|
}
|
|
return selected
|
|
}
|
|
|
|
type Cache interface {
|
|
RemoveFromCache(id string)
|
|
}
|
|
|
|
func newPathAndCost(path []*model.Router, cost int64) *PathAndCost {
|
|
if cost > (1 << 20) {
|
|
cost = 1 << 20
|
|
}
|
|
return &PathAndCost{
|
|
path: path,
|
|
cost: uint32(cost),
|
|
}
|
|
}
|
|
|
|
type PathAndCost struct {
|
|
path []*model.Router
|
|
cost uint32
|
|
}
|
|
|
|
type InvalidCircuitError struct {
|
|
circuitId string
|
|
}
|
|
|
|
func (err InvalidCircuitError) Error() string {
|
|
return fmt.Sprintf("invalid circuit (%s)", err.circuitId)
|
|
}
|