mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 16:55:41 +00:00
ecbdb92ecb
routerConnectChurnLimit predates the per-router connect lock. It was added alongside the ability for a new control channel to take over from an established one, as the guard on how often that may happen, and it was also the only thing keeping two connections for one router out of the connected map. That second job is gone: at most one connection per router is now enforced under the per-router lock, where the decision is atomic. The check in the accept path runs against the connected map with no lock held, so it can only refuse a connection early that would be refused there anyway. Its first job remains, and is now the only thing doing it. ConnectRouter always displaces an occupant it does not recognise, so without the limit a spurious first-connection hello would tear down a healthy control channel and make the router redial. Nothing said so, and the field carried no godoc at all. - documents on the option what it protects, that it is churn policy rather than the uniqueness guarantee, and that zero always allows takeover - extracts the decision so it can be tested without standing up a network, and tests it: protected when just established, protected part way through the window, displaceable once past it, and never protected at zero The struct's field alignment shifts because a comment ends gofmt's alignment group; that part of the diff is whitespace only. Behaviour is unchanged. Worth noting for readers of the option: past the window, the established connection is now displaced and the connect refused, so the router redials into the freed slot, where previously the arriving connection took over directly. Same end state, one extra round trip, and nothing unvetted is registered on the way.
157 lines
6.7 KiB
Go
157 lines
6.7 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 (
|
|
"crypto/sha1"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/channel/v5"
|
|
"github.com/openziti/identity"
|
|
"github.com/openziti/ziti/v2/common/cert"
|
|
"github.com/openziti/ziti/v2/controller/model"
|
|
"github.com/openziti/ziti/v2/controller/network"
|
|
)
|
|
|
|
type ConnectHandler struct {
|
|
identity identity.Identity
|
|
network *network.Network
|
|
|
|
// separatelyValidatedTypes holds the control-channel type headers that are dispatched to a
|
|
// separate, self-validating acceptor (currently the raft mesh, when clustering is enabled).
|
|
separatelyValidatedTypes map[string]struct{}
|
|
}
|
|
|
|
func NewConnectHandler(identity identity.Identity, network *network.Network) *ConnectHandler {
|
|
return &ConnectHandler{
|
|
identity: identity,
|
|
network: network,
|
|
}
|
|
}
|
|
|
|
// SetSeparatelyValidatedChannelTypes records the control-channel type headers that are dispatched to a
|
|
// separate, self-validating acceptor (e.g. the raft mesh). Connections carrying one of these types are
|
|
// skipped by HandleConnection; everything else - router control channel types, unrecognized types, and
|
|
// legacy (no type header) connections, all of which the dispatcher routes to the router control
|
|
// acceptor - is validated here. This must be populated before the listener begins accepting.
|
|
func (self *ConnectHandler) SetSeparatelyValidatedChannelTypes(types map[string]struct{}) {
|
|
self.separatelyValidatedTypes = types
|
|
}
|
|
|
|
// isSeparatelyValidated reports whether the connection's channel type is handled by a separate,
|
|
// self-validating acceptor and therefore must not be validated as a router control connection here.
|
|
func (self *ConnectHandler) isSeparatelyValidated(hello *channel.Hello) bool {
|
|
underlayType, found := hello.Headers[channel.TypeHeader]
|
|
if !found {
|
|
return false
|
|
}
|
|
_, ok := self.separatelyValidatedTypes[string(underlayType)]
|
|
return ok
|
|
}
|
|
|
|
// isFirstCtrlConnection reports whether this hello establishes a new channel rather than adding an
|
|
// underlay to an existing grouped channel. A legacy (non-grouped) dial is always a new channel; for a
|
|
// grouped dial only the connection carrying IsFirstGroupConnection is.
|
|
func isFirstCtrlConnection(hello *channel.Hello) bool {
|
|
headers := channel.Headers(hello.Headers)
|
|
if grouped, _ := headers.GetBoolHeader(channel.IsGroupedHeader); !grouped {
|
|
return true
|
|
}
|
|
first, _ := headers.GetBoolHeader(channel.IsFirstGroupConnection)
|
|
return first
|
|
}
|
|
|
|
// withinChurnLimit reports whether an established connection is too new to be displaced by a new one.
|
|
//
|
|
// This is admission policy, not the uniqueness guarantee. At most one connection per router is enforced
|
|
// under the per-router lock in Network.ConnectRouter; this runs against the connected map with no lock
|
|
// held, so it can only avoid paying for a bind that would be refused there anyway.
|
|
//
|
|
// Displacing an established connection costs a round trip: the occupant's teardown runs, the connect is
|
|
// refused, and the router redials into the freed slot. A connection that has only just been established
|
|
// is therefore protected for churnLimit, so a flapping router cannot thrash a working channel. A zero
|
|
// limit disables the protection, making every new connection able to displace the current one.
|
|
func withinChurnLimit(connected *model.Router, churnLimit time.Duration) bool {
|
|
return time.Since(connected.ConnectTime) < churnLimit
|
|
}
|
|
|
|
func (self *ConnectHandler) HandleConnection(hello *channel.Hello, certificates []*x509.Certificate) error {
|
|
// Connections whose channel type is handled by a separate, self-validating acceptor (e.g. the raft
|
|
// mesh) are validated there, so skip them. Everything else - router control channel types,
|
|
// unrecognized types, and legacy (no type header) connections - is dispatched to the router control
|
|
// acceptor and must be validated here.
|
|
if self.isSeparatelyValidated(hello) {
|
|
return nil
|
|
}
|
|
|
|
id := hello.IdToken
|
|
|
|
log := pfxlog.Logger().WithField("routerId", id)
|
|
|
|
if len(certificates) == 0 {
|
|
return fmt.Errorf("no certificates provided, unable to verify dialer, routerId: %v", id)
|
|
}
|
|
|
|
// Verify the peer's leaf certificate (certificates[0], the certificate whose private key the TLS
|
|
// handshake proved) chains to the controller CA, and bind the router fingerprint check to that
|
|
// verified leaf. Matching the enrolled fingerprint against any presented certificate would let a
|
|
// peer present its own leaf followed by a target router's public certificate and pass without that
|
|
// router's private key.
|
|
leaf, err := cert.VerifyLeafCertChain(self.identity.CA(), certificates)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to verify dialer, routerId: %v: %w", id, err)
|
|
}
|
|
fingerprint := fmt.Sprintf("%x", sha1.Sum(leaf.Raw))
|
|
log.Debugf("peer leaf certificate fingerprint [%s], common name [%s]", fingerprint, leaf.Subject.CommonName)
|
|
|
|
// The churn / already-connected guard applies only when establishing a new channel. Additional
|
|
// underlays of an existing grouped control channel legitimately arrive while the router is already
|
|
// connected and must not be rejected here.
|
|
if isFirstCtrlConnection(hello) {
|
|
if router := self.network.GetConnectedRouter(id); router != nil {
|
|
if withinChurnLimit(router, self.network.GetOptions().RouterConnectChurnLimit) {
|
|
log.WithField("routerName", router.Name).Error("router already connected and churn threshold not met")
|
|
return fmt.Errorf("router already connected id: %s, name: %s", id, router.Name)
|
|
}
|
|
log.WithField("routerName", router.Name).Warn("router already connected, but churn threshold met. replacing connection")
|
|
}
|
|
}
|
|
|
|
if r, err := self.network.GetRouter(id); err == nil {
|
|
if r.Fingerprint == nil {
|
|
log.Error("router enrollment incomplete")
|
|
return fmt.Errorf("router enrollment incomplete, routerId: %v", id)
|
|
}
|
|
if fingerprint != *r.Fingerprint {
|
|
log.WithField("fp", *r.Fingerprint).WithField("givenFp", fingerprint).Error("router fingerprint mismatch")
|
|
return fmt.Errorf("incorrect fingerprint/unenrolled router, routerId: %v, given fingerprint: %v", id, fingerprint)
|
|
}
|
|
if r.Disabled {
|
|
log.Error("router disabled")
|
|
return fmt.Errorf("router disabld, routerId: %v", id)
|
|
}
|
|
} else {
|
|
log.Error("unknown/unenrolled router")
|
|
return fmt.Errorf("unknown/unenrolled router, routerId: %v", id)
|
|
}
|
|
|
|
return nil
|
|
}
|