702 lines
20 KiB
Go
702 lines
20 KiB
Go
package hscontrol
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"net/http"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/db"
|
|
"github.com/juanfont/headscale/hscontrol/mapper"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/sasha-s/go-deadlock"
|
|
xslices "golang.org/x/exp/slices"
|
|
"gorm.io/gorm"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
const (
|
|
keepAliveInterval = 50 * time.Second
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
const nodeNameContextKey = contextKey("nodeName")
|
|
|
|
type mapSession struct {
|
|
h *Headscale
|
|
req tailcfg.MapRequest
|
|
ctx context.Context
|
|
capVer tailcfg.CapabilityVersion
|
|
mapper *mapper.Mapper
|
|
|
|
cancelChMu deadlock.Mutex
|
|
|
|
ch chan types.StateUpdate
|
|
cancelCh chan struct{}
|
|
cancelChOpen bool
|
|
|
|
keepAlive time.Duration
|
|
keepAliveTicker *time.Ticker
|
|
|
|
node *types.Node
|
|
w http.ResponseWriter
|
|
|
|
warnf func(string, ...any)
|
|
infof func(string, ...any)
|
|
tracef func(string, ...any)
|
|
errf func(error, string, ...any)
|
|
}
|
|
|
|
func (h *Headscale) newMapSession(
|
|
ctx context.Context,
|
|
req tailcfg.MapRequest,
|
|
w http.ResponseWriter,
|
|
node *types.Node,
|
|
) *mapSession {
|
|
warnf, infof, tracef, errf := logPollFunc(req, node)
|
|
|
|
var updateChan chan types.StateUpdate
|
|
if req.Stream {
|
|
// Use a buffered channel in case a node is not fully ready
|
|
// to receive a message to make sure we dont block the entire
|
|
// notifier.
|
|
updateChan = make(chan types.StateUpdate, h.cfg.Tuning.NodeMapSessionBufferedChanSize)
|
|
updateChan <- types.StateUpdate{
|
|
Type: types.StateFullUpdate,
|
|
}
|
|
}
|
|
|
|
ka := keepAliveInterval + (time.Duration(rand.IntN(9000)) * time.Millisecond)
|
|
|
|
return &mapSession{
|
|
h: h,
|
|
ctx: ctx,
|
|
req: req,
|
|
w: w,
|
|
node: node,
|
|
capVer: req.Version,
|
|
mapper: h.mapper,
|
|
|
|
ch: updateChan,
|
|
cancelCh: make(chan struct{}),
|
|
cancelChOpen: true,
|
|
|
|
keepAlive: ka,
|
|
keepAliveTicker: nil,
|
|
|
|
// Loggers
|
|
warnf: warnf,
|
|
infof: infof,
|
|
tracef: tracef,
|
|
errf: errf,
|
|
}
|
|
}
|
|
|
|
func (m *mapSession) close() {
|
|
m.cancelChMu.Lock()
|
|
defer m.cancelChMu.Unlock()
|
|
|
|
if !m.cancelChOpen {
|
|
mapResponseClosed.WithLabelValues("chanclosed").Inc()
|
|
return
|
|
}
|
|
|
|
m.tracef("mapSession (%p) sending message on cancel chan", m)
|
|
select {
|
|
case m.cancelCh <- struct{}{}:
|
|
mapResponseClosed.WithLabelValues("sent").Inc()
|
|
m.tracef("mapSession (%p) sent message on cancel chan", m)
|
|
case <-time.After(30 * time.Second):
|
|
mapResponseClosed.WithLabelValues("timeout").Inc()
|
|
m.tracef("mapSession (%p) timed out sending close message", m)
|
|
}
|
|
}
|
|
|
|
func (m *mapSession) isStreaming() bool {
|
|
return m.req.Stream && !m.req.ReadOnly
|
|
}
|
|
|
|
func (m *mapSession) isEndpointUpdate() bool {
|
|
return !m.req.Stream && !m.req.ReadOnly && m.req.OmitPeers
|
|
}
|
|
|
|
func (m *mapSession) isReadOnlyUpdate() bool {
|
|
return !m.req.Stream && m.req.OmitPeers && m.req.ReadOnly
|
|
}
|
|
|
|
func (m *mapSession) resetKeepAlive() {
|
|
m.keepAliveTicker.Reset(m.keepAlive)
|
|
}
|
|
|
|
func (m *mapSession) beforeServeLongPoll() {
|
|
if m.node.IsEphemeral() {
|
|
m.h.ephemeralGC.Cancel(m.node.ID)
|
|
}
|
|
}
|
|
|
|
func (m *mapSession) afterServeLongPoll() {
|
|
if m.node.IsEphemeral() {
|
|
m.h.ephemeralGC.Schedule(m.node.ID, m.h.cfg.EphemeralNodeInactivityTimeout)
|
|
}
|
|
}
|
|
|
|
// serve handles non-streaming requests.
|
|
func (m *mapSession) serve() {
|
|
// TODO(kradalby): A set todos to harden:
|
|
// - func to tell the stream to die, readonly -> false, !stream && omitpeers -> false, true
|
|
|
|
// This is the mechanism where the node gives us information about its
|
|
// current configuration.
|
|
//
|
|
// If OmitPeers is true, Stream is false, and ReadOnly is false,
|
|
// then te server will let clients update their endpoints without
|
|
// breaking existing long-polling (Stream == true) connections.
|
|
// In this case, the server can omit the entire response; the client
|
|
// only checks the HTTP response status code.
|
|
//
|
|
// This is what Tailscale calls a Lite update, the client ignores
|
|
// the response and just wants a 200.
|
|
// !req.stream && !req.ReadOnly && req.OmitPeers
|
|
//
|
|
// TODO(kradalby): remove ReadOnly when we only support capVer 68+
|
|
if m.isEndpointUpdate() {
|
|
m.handleEndpointUpdate()
|
|
|
|
return
|
|
}
|
|
|
|
// ReadOnly is whether the client just wants to fetch the
|
|
// MapResponse, without updating their Endpoints. The
|
|
// Endpoints field will be ignored and LastSeen will not be
|
|
// updated and peers will not be notified of changes.
|
|
//
|
|
// The intended use is for clients to discover the DERP map at
|
|
// start-up before their first real endpoint update.
|
|
if m.isReadOnlyUpdate() {
|
|
m.handleReadOnlyRequest()
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// serveLongPoll ensures the node gets the appropriate updates from either
|
|
// polling or immediate responses.
|
|
//
|
|
//nolint:gocyclo
|
|
func (m *mapSession) serveLongPoll() {
|
|
m.beforeServeLongPoll()
|
|
|
|
// Clean up the session when the client disconnects
|
|
defer func() {
|
|
m.cancelChMu.Lock()
|
|
m.cancelChOpen = false
|
|
close(m.cancelCh)
|
|
m.cancelChMu.Unlock()
|
|
|
|
// only update node status if the node channel was removed.
|
|
// in principal, it will be removed, but the client rapidly
|
|
// reconnects, the channel might be of another connection.
|
|
// In that case, it is not closed and the node is still online.
|
|
if m.h.nodeNotifier.RemoveNode(m.node.ID, m.ch) {
|
|
// Failover the node's routes if any.
|
|
m.h.updateNodeOnlineStatus(false, m.node)
|
|
m.pollFailoverRoutes("node closing connection", m.node)
|
|
}
|
|
|
|
m.afterServeLongPoll()
|
|
m.infof("node has disconnected, mapSession: %p, chan: %p", m, m.ch)
|
|
}()
|
|
|
|
// Set up the client stream
|
|
m.h.pollNetMapStreamWG.Add(1)
|
|
defer m.h.pollNetMapStreamWG.Done()
|
|
|
|
m.pollFailoverRoutes("node connected", m.node)
|
|
|
|
// Upgrade the writer to a ResponseController
|
|
rc := http.NewResponseController(m.w)
|
|
|
|
// Longpolling will break if there is a write timeout,
|
|
// so it needs to be disabled.
|
|
rc.SetWriteDeadline(time.Time{})
|
|
|
|
ctx, cancel := context.WithCancel(context.WithValue(m.ctx, nodeNameContextKey, m.node.Hostname))
|
|
defer cancel()
|
|
|
|
m.keepAliveTicker = time.NewTicker(m.keepAlive)
|
|
|
|
m.h.nodeNotifier.AddNode(m.node.ID, m.ch)
|
|
go m.h.updateNodeOnlineStatus(true, m.node)
|
|
|
|
m.infof("node has connected, mapSession: %p, chan: %p", m, m.ch)
|
|
|
|
// Loop through updates and continuously send them to the
|
|
// client.
|
|
for {
|
|
// consume channels with update, keep alives or "batch" blocking signals
|
|
select {
|
|
case <-m.cancelCh:
|
|
m.tracef("poll cancelled received")
|
|
mapResponseEnded.WithLabelValues("cancelled").Inc()
|
|
return
|
|
|
|
case <-ctx.Done():
|
|
m.tracef("poll context done")
|
|
mapResponseEnded.WithLabelValues("done").Inc()
|
|
return
|
|
|
|
// Consume updates sent to node
|
|
case update, ok := <-m.ch:
|
|
if !ok {
|
|
m.tracef("update channel closed, streaming session is likely being replaced")
|
|
return
|
|
}
|
|
|
|
// If the node has been removed from headscale, close the stream
|
|
if slices.Contains(update.Removed, m.node.ID) {
|
|
m.tracef("node removed, closing stream")
|
|
return
|
|
}
|
|
|
|
m.tracef("received stream update: %s %s", update.Type.String(), update.Message)
|
|
mapResponseUpdateReceived.WithLabelValues(update.Type.String()).Inc()
|
|
|
|
var data []byte
|
|
var err error
|
|
var lastMessage string
|
|
|
|
// Ensure the node object is updated, for example, there
|
|
// might have been a hostinfo update in a sidechannel
|
|
// which contains data needed to generate a map response.
|
|
m.node, err = m.h.db.GetNodeByID(m.node.ID)
|
|
if err != nil {
|
|
m.errf(err, "Could not get machine from db")
|
|
|
|
return
|
|
}
|
|
|
|
updateType := "full"
|
|
switch update.Type {
|
|
case types.StateFullUpdate:
|
|
m.tracef("Sending Full MapResponse")
|
|
data, err = m.mapper.FullMapResponse(m.req, m.node, m.h.ACLPolicy, fmt.Sprintf("from mapSession: %p, stream: %t", m, m.isStreaming()))
|
|
case types.StatePeerChanged:
|
|
changed := make(map[types.NodeID]bool, len(update.ChangeNodes))
|
|
|
|
for _, nodeID := range update.ChangeNodes {
|
|
changed[nodeID] = true
|
|
}
|
|
|
|
lastMessage = update.Message
|
|
m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage))
|
|
data, err = m.mapper.PeerChangedResponse(m.req, m.node, changed, update.ChangePatches, m.h.ACLPolicy, lastMessage)
|
|
updateType = "change"
|
|
|
|
case types.StatePeerChangedPatch:
|
|
m.tracef(fmt.Sprintf("Sending Changed Patch MapResponse: %v", lastMessage))
|
|
data, err = m.mapper.PeerChangedPatchResponse(m.req, m.node, update.ChangePatches, m.h.ACLPolicy)
|
|
updateType = "patch"
|
|
case types.StatePeerRemoved:
|
|
changed := make(map[types.NodeID]bool, len(update.Removed))
|
|
|
|
for _, nodeID := range update.Removed {
|
|
changed[nodeID] = false
|
|
}
|
|
m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage))
|
|
data, err = m.mapper.PeerChangedResponse(m.req, m.node, changed, update.ChangePatches, m.h.ACLPolicy, lastMessage)
|
|
updateType = "remove"
|
|
case types.StateSelfUpdate:
|
|
lastMessage = update.Message
|
|
m.tracef(fmt.Sprintf("Sending Changed MapResponse: %v", lastMessage))
|
|
// create the map so an empty (self) update is sent
|
|
data, err = m.mapper.PeerChangedResponse(m.req, m.node, make(map[types.NodeID]bool), update.ChangePatches, m.h.ACLPolicy, lastMessage)
|
|
updateType = "remove"
|
|
case types.StateDERPUpdated:
|
|
m.tracef("Sending DERPUpdate MapResponse")
|
|
data, err = m.mapper.DERPMapResponse(m.req, m.node, m.h.DERPMap)
|
|
updateType = "derp"
|
|
}
|
|
|
|
if err != nil {
|
|
m.errf(err, "Could not get the create map update")
|
|
|
|
return
|
|
}
|
|
|
|
// Only send update if there is change
|
|
if data != nil {
|
|
startWrite := time.Now()
|
|
_, err = m.w.Write(data)
|
|
if err != nil {
|
|
mapResponseSent.WithLabelValues("error", updateType).Inc()
|
|
m.errf(err, "could not write the map response(%s), for mapSession: %p", update.Type.String(), m)
|
|
return
|
|
}
|
|
|
|
err = rc.Flush()
|
|
if err != nil {
|
|
mapResponseSent.WithLabelValues("error", updateType).Inc()
|
|
m.errf(err, "flushing the map response to client, for mapSession: %p", m)
|
|
return
|
|
}
|
|
|
|
log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", m.node.MachineKey.String()).Msg("finished writing mapresp to node")
|
|
|
|
if debugHighCardinalityMetrics {
|
|
mapResponseLastSentSeconds.WithLabelValues(updateType, m.node.ID.String()).Set(float64(time.Now().Unix()))
|
|
}
|
|
mapResponseSent.WithLabelValues("ok", updateType).Inc()
|
|
m.tracef("update sent")
|
|
m.resetKeepAlive()
|
|
}
|
|
|
|
case <-m.keepAliveTicker.C:
|
|
data, err := m.mapper.KeepAliveResponse(m.req, m.node)
|
|
if err != nil {
|
|
m.errf(err, "Error generating the keep alive msg")
|
|
mapResponseSent.WithLabelValues("error", "keepalive").Inc()
|
|
return
|
|
}
|
|
_, err = m.w.Write(data)
|
|
if err != nil {
|
|
m.errf(err, "Cannot write keep alive message")
|
|
mapResponseSent.WithLabelValues("error", "keepalive").Inc()
|
|
return
|
|
}
|
|
err = rc.Flush()
|
|
if err != nil {
|
|
m.errf(err, "flushing keep alive to client, for mapSession: %p", m)
|
|
mapResponseSent.WithLabelValues("error", "keepalive").Inc()
|
|
return
|
|
}
|
|
|
|
if debugHighCardinalityMetrics {
|
|
mapResponseLastSentSeconds.WithLabelValues("keepalive", m.node.ID.String()).Set(float64(time.Now().Unix()))
|
|
}
|
|
mapResponseSent.WithLabelValues("ok", "keepalive").Inc()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *mapSession) pollFailoverRoutes(where string, node *types.Node) {
|
|
update, err := db.Write(m.h.db.DB, func(tx *gorm.DB) (*types.StateUpdate, error) {
|
|
return db.FailoverNodeRoutesIfNeccessary(tx, m.h.nodeNotifier.LikelyConnectedMap(), node)
|
|
})
|
|
if err != nil {
|
|
m.errf(err, fmt.Sprintf("failed to ensure failover routes, %s", where))
|
|
|
|
return
|
|
}
|
|
|
|
if update != nil && !update.Empty() {
|
|
ctx := types.NotifyCtx(context.Background(), fmt.Sprintf("poll-%s-routes-ensurefailover", strings.ReplaceAll(where, " ", "-")), node.Hostname)
|
|
m.h.nodeNotifier.NotifyWithIgnore(ctx, *update, node.ID)
|
|
}
|
|
}
|
|
|
|
// updateNodeOnlineStatus records the last seen status of a node and notifies peers
|
|
// about change in their online/offline status.
|
|
// It takes a StateUpdateType of either StatePeerOnlineChanged or StatePeerOfflineChanged.
|
|
func (h *Headscale) updateNodeOnlineStatus(online bool, node *types.Node) {
|
|
change := &tailcfg.PeerChange{
|
|
NodeID: tailcfg.NodeID(node.ID),
|
|
Online: &online,
|
|
}
|
|
|
|
if !online {
|
|
now := time.Now()
|
|
|
|
// lastSeen is only relevant if the node is disconnected.
|
|
node.LastSeen = &now
|
|
change.LastSeen = &now
|
|
|
|
err := h.db.Write(func(tx *gorm.DB) error {
|
|
return db.SetLastSeen(tx, node.ID, *node.LastSeen)
|
|
})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Cannot update node LastSeen")
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-onlinestatus", node.Hostname)
|
|
h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdate{
|
|
Type: types.StatePeerChangedPatch,
|
|
ChangePatches: []*tailcfg.PeerChange{
|
|
change,
|
|
},
|
|
}, node.ID)
|
|
}
|
|
|
|
func (m *mapSession) handleEndpointUpdate() {
|
|
m.tracef("received endpoint update")
|
|
|
|
change := m.node.PeerChangeFromMapRequest(m.req)
|
|
|
|
online := m.h.nodeNotifier.IsLikelyConnected(m.node.ID)
|
|
change.Online = &online
|
|
|
|
m.node.ApplyPeerChange(&change)
|
|
|
|
sendUpdate, routesChanged := hostInfoChanged(m.node.Hostinfo, m.req.Hostinfo)
|
|
|
|
// The node might not set NetInfo if it has not changed and if
|
|
// the full HostInfo object is overwritten, the information is lost.
|
|
// If there is no NetInfo, keep the previous one.
|
|
// From 1.66 the client only sends it if changed:
|
|
// https://github.com/tailscale/tailscale/commit/e1011f138737286ecf5123ff887a7a5800d129a2
|
|
// TODO(kradalby): evaulate if we need better comparing of hostinfo
|
|
// before we take the changes.
|
|
if m.req.Hostinfo.NetInfo == nil && m.node.Hostinfo != nil {
|
|
m.req.Hostinfo.NetInfo = m.node.Hostinfo.NetInfo
|
|
}
|
|
m.node.Hostinfo = m.req.Hostinfo
|
|
|
|
logTracePeerChange(m.node.Hostname, sendUpdate, &change)
|
|
|
|
// If there is no changes and nothing to save,
|
|
// return early.
|
|
if peerChangeEmpty(change) && !sendUpdate {
|
|
mapResponseEndpointUpdates.WithLabelValues("noop").Inc()
|
|
return
|
|
}
|
|
|
|
// Check if the Hostinfo of the node has changed.
|
|
// If it has changed, check if there has been a change to
|
|
// the routable IPs of the host and update them in
|
|
// the database. Then send a Changed update
|
|
// (containing the whole node object) to peers to inform about
|
|
// the route change.
|
|
// If the hostinfo has changed, but not the routes, just update
|
|
// hostinfo and let the function continue.
|
|
if routesChanged {
|
|
var err error
|
|
_, err = m.h.db.SaveNodeRoutes(m.node)
|
|
if err != nil {
|
|
m.errf(err, "Error processing node routes")
|
|
http.Error(m.w, "", http.StatusInternalServerError)
|
|
mapResponseEndpointUpdates.WithLabelValues("error").Inc()
|
|
|
|
return
|
|
}
|
|
|
|
if m.h.ACLPolicy != nil {
|
|
// update routes with peer information
|
|
err := m.h.db.EnableAutoApprovedRoutes(m.h.ACLPolicy, m.node)
|
|
if err != nil {
|
|
m.errf(err, "Error running auto approved routes")
|
|
mapResponseEndpointUpdates.WithLabelValues("error").Inc()
|
|
}
|
|
}
|
|
|
|
// Send an update to the node itself with to ensure it
|
|
// has an updated packetfilter allowing the new route
|
|
// if it is defined in the ACL.
|
|
ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-self-hostinfochange", m.node.Hostname)
|
|
m.h.nodeNotifier.NotifyByNodeID(
|
|
ctx,
|
|
types.StateUpdate{
|
|
Type: types.StateSelfUpdate,
|
|
ChangeNodes: []types.NodeID{m.node.ID},
|
|
},
|
|
m.node.ID)
|
|
}
|
|
|
|
// Check if there has been a change to Hostname and update them
|
|
// in the database. Then send a Changed update
|
|
// (containing the whole node object) to peers to inform about
|
|
// the hostname change.
|
|
m.node.ApplyHostnameFromHostInfo(m.req.Hostinfo)
|
|
|
|
if err := m.h.db.DB.Save(m.node).Error; err != nil {
|
|
m.errf(err, "Failed to persist/update node in the database")
|
|
http.Error(m.w, "", http.StatusInternalServerError)
|
|
mapResponseEndpointUpdates.WithLabelValues("error").Inc()
|
|
|
|
return
|
|
}
|
|
|
|
ctx := types.NotifyCtx(context.Background(), "poll-nodeupdate-peers-patch", m.node.Hostname)
|
|
m.h.nodeNotifier.NotifyWithIgnore(
|
|
ctx,
|
|
types.StateUpdate{
|
|
Type: types.StatePeerChanged,
|
|
ChangeNodes: []types.NodeID{m.node.ID},
|
|
Message: "called from handlePoll -> update",
|
|
},
|
|
m.node.ID,
|
|
)
|
|
|
|
m.w.WriteHeader(http.StatusOK)
|
|
mapResponseEndpointUpdates.WithLabelValues("ok").Inc()
|
|
|
|
return
|
|
}
|
|
|
|
func (m *mapSession) handleReadOnlyRequest() {
|
|
m.tracef("Client asked for a lite update, responding without peers")
|
|
|
|
mapResp, err := m.mapper.ReadOnlyMapResponse(m.req, m.node, m.h.ACLPolicy)
|
|
if err != nil {
|
|
m.errf(err, "Failed to create MapResponse")
|
|
http.Error(m.w, "", http.StatusInternalServerError)
|
|
mapResponseReadOnly.WithLabelValues("error").Inc()
|
|
return
|
|
}
|
|
|
|
m.w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
m.w.WriteHeader(http.StatusOK)
|
|
_, err = m.w.Write(mapResp)
|
|
if err != nil {
|
|
m.errf(err, "Failed to write response")
|
|
mapResponseReadOnly.WithLabelValues("error").Inc()
|
|
return
|
|
}
|
|
|
|
m.w.WriteHeader(http.StatusOK)
|
|
mapResponseReadOnly.WithLabelValues("ok").Inc()
|
|
|
|
return
|
|
}
|
|
|
|
func logTracePeerChange(hostname string, hostinfoChange bool, change *tailcfg.PeerChange) {
|
|
trace := log.Trace().Uint64("node.id", uint64(change.NodeID)).Str("hostname", hostname)
|
|
|
|
if change.Key != nil {
|
|
trace = trace.Str("node_key", change.Key.ShortString())
|
|
}
|
|
|
|
if change.DiscoKey != nil {
|
|
trace = trace.Str("disco_key", change.DiscoKey.ShortString())
|
|
}
|
|
|
|
if change.Online != nil {
|
|
trace = trace.Bool("online", *change.Online)
|
|
}
|
|
|
|
if change.Endpoints != nil {
|
|
eps := make([]string, len(change.Endpoints))
|
|
for idx, ep := range change.Endpoints {
|
|
eps[idx] = ep.String()
|
|
}
|
|
|
|
trace = trace.Strs("endpoints", eps)
|
|
}
|
|
|
|
if hostinfoChange {
|
|
trace = trace.Bool("hostinfo_changed", hostinfoChange)
|
|
}
|
|
|
|
if change.DERPRegion != 0 {
|
|
trace = trace.Int("derp_region", change.DERPRegion)
|
|
}
|
|
|
|
trace.Time("last_seen", *change.LastSeen).Msg("PeerChange received")
|
|
}
|
|
|
|
func peerChangeEmpty(chng tailcfg.PeerChange) bool {
|
|
return chng.Key == nil &&
|
|
chng.DiscoKey == nil &&
|
|
chng.Online == nil &&
|
|
chng.Endpoints == nil &&
|
|
chng.DERPRegion == 0 &&
|
|
chng.LastSeen == nil &&
|
|
chng.KeyExpiry == nil
|
|
}
|
|
|
|
func logPollFunc(
|
|
mapRequest tailcfg.MapRequest,
|
|
node *types.Node,
|
|
) (func(string, ...any), func(string, ...any), func(string, ...any), func(error, string, ...any)) {
|
|
return func(msg string, a ...any) {
|
|
log.Warn().
|
|
Caller().
|
|
Bool("readOnly", mapRequest.ReadOnly).
|
|
Bool("omitPeers", mapRequest.OmitPeers).
|
|
Bool("stream", mapRequest.Stream).
|
|
Uint64("node.id", node.ID.Uint64()).
|
|
Str("node", node.Hostname).
|
|
Msgf(msg, a...)
|
|
},
|
|
func(msg string, a ...any) {
|
|
log.Info().
|
|
Caller().
|
|
Bool("readOnly", mapRequest.ReadOnly).
|
|
Bool("omitPeers", mapRequest.OmitPeers).
|
|
Bool("stream", mapRequest.Stream).
|
|
Uint64("node.id", node.ID.Uint64()).
|
|
Str("node", node.Hostname).
|
|
Msgf(msg, a...)
|
|
},
|
|
func(msg string, a ...any) {
|
|
log.Trace().
|
|
Caller().
|
|
Bool("readOnly", mapRequest.ReadOnly).
|
|
Bool("omitPeers", mapRequest.OmitPeers).
|
|
Bool("stream", mapRequest.Stream).
|
|
Uint64("node.id", node.ID.Uint64()).
|
|
Str("node", node.Hostname).
|
|
Msgf(msg, a...)
|
|
},
|
|
func(err error, msg string, a ...any) {
|
|
log.Error().
|
|
Caller().
|
|
Bool("readOnly", mapRequest.ReadOnly).
|
|
Bool("omitPeers", mapRequest.OmitPeers).
|
|
Bool("stream", mapRequest.Stream).
|
|
Uint64("node.id", node.ID.Uint64()).
|
|
Str("node", node.Hostname).
|
|
Err(err).
|
|
Msgf(msg, a...)
|
|
}
|
|
}
|
|
|
|
// hostInfoChanged reports if hostInfo has changed in two ways,
|
|
// - first bool reports if an update needs to be sent to nodes
|
|
// - second reports if there has been changes to routes
|
|
// the caller can then use this info to save and update nodes
|
|
// and routes as needed.
|
|
func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) {
|
|
if old.Equal(new) {
|
|
return false, false
|
|
}
|
|
|
|
if old == nil && new != nil {
|
|
return true, true
|
|
}
|
|
|
|
// Routes
|
|
oldRoutes := make([]netip.Prefix, 0)
|
|
if old != nil {
|
|
oldRoutes = old.RoutableIPs
|
|
}
|
|
newRoutes := new.RoutableIPs
|
|
|
|
tsaddr.SortPrefixes(oldRoutes)
|
|
tsaddr.SortPrefixes(newRoutes)
|
|
|
|
if !xslices.Equal(oldRoutes, newRoutes) {
|
|
return true, true
|
|
}
|
|
|
|
// Services is mostly useful for discovery and not critical,
|
|
// except for peerapi, which is how nodes talk to eachother.
|
|
// If peerapi was not part of the initial mapresponse, we
|
|
// need to make sure its sent out later as it is needed for
|
|
// Taildrop.
|
|
// TODO(kradalby): Length comparison is a bit naive, replace.
|
|
if len(old.Services) != len(new.Services) {
|
|
return true, false
|
|
}
|
|
|
|
return false, false
|
|
}
|