mirror of
https://github.com/hanzozt/zt.git
synced 2026-11-03 05:01:58 +00:00
431 lines
14 KiB
Go
431 lines
14 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 xgress_edge_tunnel_v2
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/cenkalti/backoff/v4"
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/channel/v3"
|
|
"github.com/openziti/channel/v3/protobufs"
|
|
"github.com/openziti/edge-api/rest_model"
|
|
"github.com/openziti/foundation/v2/concurrenz"
|
|
"github.com/openziti/sdk-golang/ziti"
|
|
"github.com/openziti/sdk-golang/ziti/edge"
|
|
"github.com/openziti/secretstream/kx"
|
|
"github.com/openziti/ziti/common/ctrl_msg"
|
|
"github.com/openziti/ziti/common/pb/edge_ctrl_pb"
|
|
"github.com/openziti/ziti/controller/idgen"
|
|
"github.com/openziti/ziti/router/posture"
|
|
"github.com/openziti/ziti/router/xgress"
|
|
"github.com/openziti/ziti/router/xgress_common"
|
|
"github.com/openziti/ziti/tunnel"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"google.golang.org/protobuf/proto"
|
|
"math"
|
|
"net"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
func newProvider(factory *Factory, tunneler *tunneler) *fabricProvider {
|
|
return &fabricProvider{
|
|
factory: factory,
|
|
tunneler: tunneler,
|
|
}
|
|
}
|
|
|
|
type fabricProvider struct {
|
|
factory *Factory
|
|
tunneler *tunneler
|
|
|
|
currentIdentity atomic.Pointer[rest_model.IdentityDetail]
|
|
}
|
|
|
|
func (self *fabricProvider) PrepForUse(string) {}
|
|
|
|
func (self *fabricProvider) GetCurrentIdentity() (*rest_model.IdentityDetail, error) {
|
|
return self.currentIdentity.Load(), nil
|
|
}
|
|
|
|
func (self *fabricProvider) GetCurrentIdentityWithBackoff() (*rest_model.IdentityDetail, error) {
|
|
return self.currentIdentity.Load(), nil
|
|
}
|
|
|
|
func (self *fabricProvider) updateIdentity(i *rest_model.IdentityDetail) {
|
|
self.currentIdentity.Store(i)
|
|
}
|
|
|
|
func (self *fabricProvider) TunnelService(service tunnel.Service, terminatorInstanceId string, conn net.Conn, halfClose bool, appData []byte) error {
|
|
keyPair, err := kx.NewKeyPair()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log := logrus.WithField("service", service.GetName())
|
|
|
|
peerData := make(map[uint32][]byte)
|
|
if service.IsEncryptionRequired() {
|
|
peerData[edge.PublicKeyHeader] = keyPair.Public()
|
|
}
|
|
if len(appData) > 0 {
|
|
peerData[edge.AppDataHeader] = appData
|
|
}
|
|
|
|
peerData[uint32(ctrl_msg.InitiatorLocalAddressHeader)] = []byte(conn.LocalAddr().String())
|
|
peerData[uint32(ctrl_msg.InitiatorRemoteAddressHeader)] = []byte(conn.RemoteAddr().String())
|
|
|
|
ctrlCh := self.factory.ctrls.AnyCtrlChannel()
|
|
if ctrlCh == nil {
|
|
errStr := "no controller available, cannot create circuit"
|
|
log.Error(errStr)
|
|
return errors.New(errStr)
|
|
}
|
|
|
|
log = log.WithField("ctrlId", ctrlCh.Id())
|
|
|
|
rdm := self.factory.stateManager.RouterDataModel()
|
|
if policy, err := posture.HasAccess(rdm, self.factory.routerConfig.Id.Token, service.GetId(), nil, edge_ctrl_pb.PolicyType_DialPolicy); err != nil && policy != nil {
|
|
return fmt.Errorf("router does not have access to service '%s' (%w)", service.GetName(), err)
|
|
}
|
|
|
|
request := &edge_ctrl_pb.CreateTunnelCircuitV2Request{
|
|
ServiceName: service.GetName(),
|
|
TerminatorInstanceId: terminatorInstanceId,
|
|
PeerData: peerData,
|
|
}
|
|
|
|
responseMsg, err := protobufs.MarshalTyped(request).WithTimeout(service.GetDialTimeout()).SendForReply(ctrlCh)
|
|
|
|
response := &edge_ctrl_pb.CreateTunnelCircuitV2Response{}
|
|
if err = xgress_common.GetResultOrFailure(responseMsg, err, response); err != nil {
|
|
log.WithError(err).Warn("failed to dial fabric")
|
|
return err
|
|
}
|
|
|
|
peerKey, peerKeyFound := response.PeerData[edge.PublicKeyHeader]
|
|
if service.IsEncryptionRequired() && !peerKeyFound {
|
|
return errors.New("service requires encryption, but public key header not returned")
|
|
}
|
|
|
|
xgConn := xgress_common.NewXgressConn(conn, halfClose, false)
|
|
|
|
if peerKeyFound {
|
|
if err = xgConn.SetupClientCrypto(keyPair, peerKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
x := xgress.NewXgress(response.CircuitId, ctrlCh.Id(), xgress.Address(response.Address), xgConn, xgress.Initiator, self.tunneler.listenOptions.Options, response.Tags)
|
|
self.tunneler.bindHandler.HandleXgressBind(x)
|
|
x.Start()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *fabricProvider) HostService(hostCtx tunnel.HostingContext) (tunnel.HostControl, error) {
|
|
id := idgen.NewUUIDString()
|
|
|
|
terminator := &tunnelTerminator{
|
|
id: id,
|
|
provider: self,
|
|
context: hostCtx,
|
|
notifyCreated: make(chan struct{}, 1),
|
|
}
|
|
|
|
self.tunneler.terminators.Set(terminator.id, terminator)
|
|
|
|
err := self.factory.env.GetRateLimiterPool().QueueWithTimeout(func() {
|
|
self.establishTerminatorWithRetry(terminator)
|
|
}, math.MaxInt64)
|
|
|
|
if err != nil { // should only happen if router is shutting down
|
|
self.tunneler.terminators.Remove(terminator.id)
|
|
return nil, err
|
|
}
|
|
|
|
return terminator, nil
|
|
}
|
|
|
|
func (self *fabricProvider) establishTerminatorWithRetry(terminator *tunnelTerminator) {
|
|
log := logrus.WithField("service", terminator.context.ServiceName())
|
|
|
|
if terminator.closed.Load() {
|
|
log.Info("not attempting to establish terminator, service not hostable")
|
|
return
|
|
}
|
|
|
|
operation := func() error {
|
|
var err error
|
|
if !terminator.created.Load() {
|
|
log.Info("attempting to establish terminator")
|
|
err = self.establishTerminator(terminator)
|
|
if err != nil && terminator.closed.Load() {
|
|
return backoff.Permanent(err)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
expBackoff := backoff.NewExponentialBackOff()
|
|
expBackoff.InitialInterval = 5 * time.Second
|
|
expBackoff.MaxInterval = 5 * time.Minute
|
|
|
|
if err := backoff.Retry(operation, expBackoff); err != nil {
|
|
log.WithError(err).Error("stopping attempts to establish terminator, service not hostable")
|
|
}
|
|
}
|
|
|
|
func (self *fabricProvider) establishTerminator(terminator *tunnelTerminator) error {
|
|
start := time.Now().UnixMilli()
|
|
log := pfxlog.Logger().
|
|
WithField("routerId", self.factory.id.Token).
|
|
WithField("service", terminator.context.ServiceName()).
|
|
WithField("terminatorId", terminator.id)
|
|
|
|
precedence := edge_ctrl_pb.TerminatorPrecedence_Default
|
|
if terminator.context.ListenOptions().Precedence == ziti.PrecedenceRequired {
|
|
precedence = edge_ctrl_pb.TerminatorPrecedence_Required
|
|
} else if terminator.context.ListenOptions().Precedence == ziti.PrecedenceFailed {
|
|
precedence = edge_ctrl_pb.TerminatorPrecedence_Failed
|
|
}
|
|
|
|
request := &edge_ctrl_pb.CreateTunnelTerminatorRequestV2{
|
|
ServiceId: terminator.context.ServiceId(),
|
|
Address: terminator.id,
|
|
Cost: uint32(terminator.context.ListenOptions().Cost),
|
|
Precedence: precedence,
|
|
InstanceId: terminator.context.ListenOptions().Identity,
|
|
StartTime: start,
|
|
}
|
|
|
|
ctrlCh := self.factory.ctrls.AnyCtrlChannel()
|
|
if ctrlCh == nil {
|
|
errStr := "no controller available, cannot create terminator"
|
|
log.Error(errStr)
|
|
return errors.New(errStr)
|
|
}
|
|
|
|
err := protobufs.MarshalTyped(request).WithTimeout(self.factory.DefaultRequestTimeout()).SendAndWaitForWire(ctrlCh)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if terminator.WaitForCreated(10 * time.Second) {
|
|
return nil
|
|
}
|
|
|
|
// return an error to indicate that we need to check if a response has come back after the next interval,
|
|
// and if not, re-send
|
|
return errors.Errorf("timeout waiting for response to create terminator request for terminator %v on service %v",
|
|
terminator.id, terminator.context.ServiceName())
|
|
}
|
|
|
|
func (self *fabricProvider) HandleTunnelResponse(msg *channel.Message, ctrlCh channel.Channel) {
|
|
log := pfxlog.Logger().WithField("routerId", self.factory.id.Token)
|
|
|
|
response := &edge_ctrl_pb.CreateTunnelTerminatorResponseV2{}
|
|
|
|
if err := proto.Unmarshal(msg.Body, response); err != nil {
|
|
log.WithError(err).Error("error unmarshalling create tunnel terminator response")
|
|
return
|
|
}
|
|
|
|
log = log.WithField("terminatorId", response.TerminatorId)
|
|
|
|
terminator, found := self.factory.tunneler.terminators.Get(response.TerminatorId)
|
|
if !found {
|
|
log.Error("no terminator found for id")
|
|
return
|
|
}
|
|
|
|
if terminator.created.CompareAndSwap(false, true) {
|
|
closeCallback := func() {
|
|
if err := self.removeTerminator(terminator); err != nil {
|
|
log.WithError(err).Error("failed to remove terminator after edge session was removed")
|
|
}
|
|
terminator.created.Store(false)
|
|
go self.establishTerminatorWithRetry(terminator)
|
|
}
|
|
|
|
terminator.closeCallback.Store(closeCallback)
|
|
|
|
if response.StartTime > 0 {
|
|
elapsedTime := time.Since(time.UnixMilli(response.StartTime))
|
|
log = log.WithField("createDuration", elapsedTime)
|
|
self.factory.metricsRegistry.Timer("xgress_edge_tunnel.terminator.create_timer").Update(elapsedTime)
|
|
}
|
|
|
|
log.Info("received terminator created notification")
|
|
} else {
|
|
log.Info("received additional terminator created notification")
|
|
}
|
|
|
|
terminator.NotifyCreated()
|
|
}
|
|
|
|
func (self *fabricProvider) removeTerminator(terminator *tunnelTerminator) error {
|
|
ctrlCh := self.factory.ctrls.AnyCtrlChannel()
|
|
if ctrlCh == nil {
|
|
return errors.New("no controller available, cannot remove terminator")
|
|
}
|
|
|
|
msg := channel.NewMessage(int32(edge_ctrl_pb.ContentType_RemoveTunnelTerminatorRequestType), []byte(terminator.id))
|
|
responseMsg, err := msg.WithTimeout(self.factory.DefaultRequestTimeout()).SendForReply(ctrlCh)
|
|
return xgress_common.CheckForFailureResult(responseMsg, err, edge_ctrl_pb.ContentType_RemoveTunnelTerminatorResponseType)
|
|
}
|
|
|
|
func (self *fabricProvider) updateTerminator(terminatorId string, cost *uint16, precedence *edge.Precedence) error {
|
|
ctrlCh := self.factory.ctrls.AnyCtrlChannel()
|
|
if ctrlCh == nil {
|
|
return errors.New("no controller available, cannot update terminator")
|
|
}
|
|
|
|
request := &edge_ctrl_pb.UpdateTunnelTerminatorRequest{
|
|
TerminatorId: terminatorId,
|
|
}
|
|
|
|
if cost != nil {
|
|
request.Cost = uint32(*cost)
|
|
request.UpdateCost = true
|
|
}
|
|
|
|
if precedence != nil {
|
|
request.Precedence = edge_ctrl_pb.TerminatorPrecedence_Default
|
|
request.UpdatePrecedence = true
|
|
if *precedence == edge.PrecedenceRequired {
|
|
request.Precedence = edge_ctrl_pb.TerminatorPrecedence_Required
|
|
} else if *precedence == edge.PrecedenceFailed {
|
|
request.Precedence = edge_ctrl_pb.TerminatorPrecedence_Failed
|
|
}
|
|
}
|
|
|
|
log := logrus.WithField("terminator", terminatorId).
|
|
WithField("precedence", request.Precedence).
|
|
WithField("cost", request.Cost).
|
|
WithField("updatingPrecedence", request.UpdatePrecedence).
|
|
WithField("updatingCost", request.UpdateCost)
|
|
|
|
log.Debug("updating terminator")
|
|
|
|
responseMsg, err := protobufs.MarshalTyped(request).WithTimeout(self.factory.DefaultRequestTimeout()).SendForReply(ctrlCh)
|
|
if err := xgress_common.CheckForFailureResult(responseMsg, err, edge_ctrl_pb.ContentType_UpdateTunnelTerminatorResponseType); err != nil {
|
|
log.WithError(err).Error("terminator update failed")
|
|
return err
|
|
}
|
|
|
|
log.Debug("terminator updated successfully")
|
|
return nil
|
|
}
|
|
|
|
func (self *fabricProvider) sendHealthEvent(terminatorId string, checkPassed bool) error {
|
|
ctrlCh := self.factory.ctrls.AnyCtrlChannel()
|
|
if ctrlCh == nil {
|
|
return errors.New("no controller available, cannot forward health event")
|
|
}
|
|
|
|
msg := channel.NewMessage(int32(edge_ctrl_pb.ContentType_TunnelHealthEventType), nil)
|
|
msg.Headers[int32(edge_ctrl_pb.Header_TerminatorId)] = []byte(terminatorId)
|
|
msg.PutBoolHeader(int32(edge_ctrl_pb.Header_CheckPassed), checkPassed)
|
|
|
|
logger := logrus.WithField("terminator", terminatorId).
|
|
WithField("checkPassed", checkPassed)
|
|
logger.Debug("sending health event")
|
|
|
|
if err := msg.WithTimeout(self.factory.ctrls.DefaultRequestTimeout()).Send(ctrlCh); err != nil {
|
|
logger.WithError(err).Error("health event send failed")
|
|
} else {
|
|
logger.Debug("health event sent")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type tunnelTerminator struct {
|
|
id string
|
|
provider *fabricProvider
|
|
context tunnel.HostingContext
|
|
created atomic.Bool
|
|
closeCallback concurrenz.AtomicValue[func()]
|
|
closed atomic.Bool
|
|
notifyCreated chan struct{}
|
|
}
|
|
|
|
func (self *tunnelTerminator) SendHealthEvent(pass bool) error {
|
|
return self.provider.sendHealthEvent(self.id, pass)
|
|
}
|
|
|
|
func (self *tunnelTerminator) NotifyCreated() {
|
|
select {
|
|
case self.notifyCreated <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (self *tunnelTerminator) WaitForCreated(timeout time.Duration) bool {
|
|
if self.created.Load() {
|
|
return true
|
|
}
|
|
select {
|
|
case <-self.notifyCreated:
|
|
case <-time.After(timeout):
|
|
}
|
|
return self.created.Load()
|
|
}
|
|
|
|
func (self *tunnelTerminator) Close() error {
|
|
if self.closed.CompareAndSwap(false, true) {
|
|
log := logrus.WithField("service", self.context.ServiceName()).
|
|
WithField("routerId", self.provider.factory.id.Token).
|
|
WithField("terminatorId", self.id)
|
|
|
|
log.Debug("closing tunnel terminator context")
|
|
self.context.OnClose()
|
|
|
|
if cb := self.closeCallback.Load(); cb != nil {
|
|
log.Debug("unregistering session listener for tunnel terminator")
|
|
cb()
|
|
}
|
|
|
|
log.Debug("removing tunnel terminator")
|
|
if err := self.provider.removeTerminator(self); err != nil {
|
|
log.WithError(err).Error("error while removing tunnel terminator")
|
|
return err
|
|
}
|
|
log.Info("removed tunnel terminator")
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *tunnelTerminator) UpdateCost(cost uint16) error {
|
|
return self.updateCostAndPrecedence(&cost, nil)
|
|
}
|
|
|
|
func (self *tunnelTerminator) UpdatePrecedence(precedence edge.Precedence) error {
|
|
return self.updateCostAndPrecedence(nil, &precedence)
|
|
}
|
|
|
|
func (self *tunnelTerminator) UpdateCostAndPrecedence(cost uint16, precedence edge.Precedence) error {
|
|
return self.updateCostAndPrecedence(&cost, &precedence)
|
|
}
|
|
|
|
func (self *tunnelTerminator) updateCostAndPrecedence(cost *uint16, precedence *edge.Precedence) error {
|
|
return self.provider.updateTerminator(self.id, cost, precedence)
|
|
}
|