Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package wgengine
|
||||
|
||||
import (
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Bird is the engine's handle on the BIRD Internet Routing Daemon
|
||||
// integration, implemented by the feature/bird package. It enables the
|
||||
// "tailscale" protocol in BIRD while this node is a primary subnet
|
||||
// router and disables it otherwise.
|
||||
//
|
||||
// Reconfig and ReconfigDone are only called from [Engine.Reconfig],
|
||||
// serialized by the engine's internal lock. Close is called from
|
||||
// [Engine.Close].
|
||||
type Bird interface {
|
||||
// Reconfig recomputes from the given self node whether this node is
|
||||
// a primary subnet router. It is called near the top of every
|
||||
// [Engine.Reconfig], before its ErrNoChanges early return. It
|
||||
// reports whether the primary subnet router state changed, which
|
||||
// suppresses the engine's ErrNoChanges early return.
|
||||
Reconfig(self tailcfg.NodeView) (changed bool)
|
||||
|
||||
// ReconfigDone is called at the end of [Engine.Reconfig], after the
|
||||
// router is configured, and applies any protocol state change
|
||||
// computed by the preceding Reconfig call.
|
||||
ReconfigDone()
|
||||
|
||||
// Close disables the protocol and closes the connection to BIRD.
|
||||
Close()
|
||||
}
|
||||
|
||||
// HookNewBird is set by the feature/bird package to construct each
|
||||
// engine's [Bird], connecting to the BIRD unix socket at socketPath.
|
||||
// If unset, BIRD integration is not linked into the binary and
|
||||
// [Config.BIRDSocket] must not be set.
|
||||
var HookNewBird feature.Hook[func(logf logger.Logf, socketPath string) (Bird, error)]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_connreject
|
||||
|
||||
package wgengine
|
||||
|
||||
import (
|
||||
"tailscale.com/net/connreject"
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
// connRejectState holds the connreject feature's per-engine storage.
|
||||
// Its definition only references [tailscale.com/net/connreject] when
|
||||
// the feature is built in; see connreject_stub.go for the no-op
|
||||
// counterpart used under -tags ts_omit_connreject.
|
||||
type connRejectState struct {
|
||||
cb syncs.AtomicValue[func(connreject.Event)]
|
||||
}
|
||||
|
||||
// SetConnRejectCallback installs a callback invoked when the engine
|
||||
// observes an outbound-direction connection rejection (an inbound TSMP
|
||||
// reject from a peer or a pendopen timeout). Passing nil uninstalls a
|
||||
// previously installed callback.
|
||||
//
|
||||
// The method is intentionally not declared on the [Engine] interface
|
||||
// so min builds do not pull in [tailscale.com/net/connreject].
|
||||
func (e *userspaceEngine) SetConnRejectCallback(fn func(connreject.Event)) {
|
||||
e.connReject.cb.Store(fn)
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debug && !ts_omit_connreject
|
||||
|
||||
package wgengine
|
||||
|
||||
import (
|
||||
"tailscale.com/net/connreject"
|
||||
"tailscale.com/net/flowtrack"
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
// notifyConnRejectTSMPRecv emits an Outgoing-direction event derived
|
||||
// from an inbound TSMP rejected header, if a callback is installed.
|
||||
func (e *userspaceEngine) notifyConnRejectTSMPRecv(rh packet.TailscaleRejectedHeader) {
|
||||
fn := e.connReject.cb.Load()
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
fn(connreject.Event{
|
||||
Direction: connreject.Outgoing,
|
||||
Proto: rh.Proto,
|
||||
Src: rh.Src,
|
||||
Dst: rh.Dst,
|
||||
Reason: rejectReasonToReason(rh.Reason),
|
||||
Source: connreject.SourceTSMPRecv,
|
||||
MaybeBroken: rh.MaybeBroken,
|
||||
})
|
||||
}
|
||||
|
||||
// notifyConnRejectOpenTimeout classifies a pendopen-timeout diagnosis
|
||||
// and emits an Outgoing-direction event, if a callback is installed and
|
||||
// the classification is not deliberately suppressed (see
|
||||
// [classifyOpenTimeout]).
|
||||
func (e *userspaceEngine) notifyConnRejectOpenTimeout(flow flowtrack.Tuple, d openTimeoutDiag) {
|
||||
fn := e.connReject.cb.Load()
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
reason, source := classifyOpenTimeout(d)
|
||||
if reason == "" {
|
||||
return
|
||||
}
|
||||
fn(connreject.Event{
|
||||
Direction: connreject.Outgoing,
|
||||
Proto: flow.Proto(),
|
||||
Src: flow.Src(),
|
||||
Dst: flow.Dst(),
|
||||
Reason: reason,
|
||||
Source: source,
|
||||
})
|
||||
}
|
||||
|
||||
// classifyOpenTimeout maps an [openTimeoutDiag] to a [connreject.Reason]
|
||||
// and [connreject.Source] for emission. A returned reason of "" means
|
||||
// the caller should not emit any event (used for the deliberately-silent
|
||||
// onlyZeroRoute case).
|
||||
//
|
||||
// If d.problem is set, the peer's TSMP-reported reason supersedes our
|
||||
// own diagnosis and the event is tagged SourceTSMPRecv (the timeout
|
||||
// confirms the previously non-terminal reject was actually terminal).
|
||||
func classifyOpenTimeout(d openTimeoutDiag) (connreject.Reason, connreject.Source) {
|
||||
if d.onlyZeroRoute {
|
||||
return "", connreject.SourceUnknown
|
||||
}
|
||||
if !d.problem.IsZero() {
|
||||
return rejectReasonToReason(d.problem), connreject.SourceTSMPRecv
|
||||
}
|
||||
switch {
|
||||
case d.noPeer:
|
||||
return connreject.ReasonNoPeer, connreject.SourcePendOpenTimeout
|
||||
case d.peerUnreachable:
|
||||
return connreject.ReasonPeerUnreachable, connreject.SourcePendOpenTimeout
|
||||
}
|
||||
return connreject.ReasonTimeout, connreject.SourcePendOpenTimeout
|
||||
}
|
||||
|
||||
// rejectReasonToReason maps a [packet.TailscaleRejectReason] to its
|
||||
// corresponding [connreject.Reason] tag.
|
||||
func rejectReasonToReason(r packet.TailscaleRejectReason) connreject.Reason {
|
||||
switch r {
|
||||
case packet.RejectedDueToACLs:
|
||||
return connreject.ReasonACL
|
||||
case packet.RejectedDueToShieldsUp:
|
||||
return connreject.ReasonShields
|
||||
case packet.RejectedDueToIPForwarding:
|
||||
return connreject.ReasonHostIPForwarding
|
||||
case packet.RejectedDueToHostFirewall:
|
||||
return connreject.ReasonHostFirewall
|
||||
case packet.RejectedDueToUnknownAppConnectorTransitIP:
|
||||
return connreject.ReasonAppConnectorTransitIPUnknown
|
||||
}
|
||||
return connreject.ReasonUnknown
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debug && ts_omit_connreject
|
||||
|
||||
package wgengine
|
||||
|
||||
import (
|
||||
"tailscale.com/net/flowtrack"
|
||||
"tailscale.com/net/packet"
|
||||
)
|
||||
|
||||
// notifyConnRejectTSMPRecv is a no-op when the connreject feature is
|
||||
// omitted at build time.
|
||||
func (*userspaceEngine) notifyConnRejectTSMPRecv(packet.TailscaleRejectedHeader) {}
|
||||
|
||||
// notifyConnRejectOpenTimeout is a no-op when the connreject feature is
|
||||
// omitted at build time.
|
||||
func (*userspaceEngine) notifyConnRejectOpenTimeout(flowtrack.Tuple, openTimeoutDiag) {}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_connreject
|
||||
|
||||
package wgengine
|
||||
|
||||
// connRejectState is the omit-build counterpart of the type in
|
||||
// connreject.go. It is an empty struct so the engine's connReject
|
||||
// field exists in both builds without referencing
|
||||
// tailscale.com/net/connreject.
|
||||
type connRejectState struct{}
|
||||
+86
-12
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package filter is a stateful packet filter.
|
||||
@@ -32,8 +32,9 @@ import (
|
||||
type Filter struct {
|
||||
logf logger.Logf
|
||||
// local4 and local6 report whether an IP is "local" to this node, for the
|
||||
// respective address family. All packets coming in over tailscale must have
|
||||
// a destination within local, regardless of the policy filter below.
|
||||
// respective address family. Inbound packets that pass the direction-agnostic
|
||||
// pre-checks and are not accepted by [Filter.IngressAllowHooks] must have a destination
|
||||
// within local to be considered by the policy filter.
|
||||
local4 func(netip.Addr) bool
|
||||
local6 func(netip.Addr) bool
|
||||
|
||||
@@ -66,8 +67,38 @@ type Filter struct {
|
||||
state *filterState
|
||||
|
||||
shieldsUp bool
|
||||
|
||||
// IngressAllowHooks are hooks that allow extensions to accept inbound
|
||||
// packets beyond the standard filter rules. Packets that are not dropped
|
||||
// by the direction-agnostic pre-check, but would be not accepted by the
|
||||
// main filter rules, including the check for destinations in the node's
|
||||
// local IP set, will be accepted if they match one of these hooks.
|
||||
// As of 2026-02-24, the ingress filter does not implement explicit drop
|
||||
// rules, but if it does, an explicitly dropped packet will be dropped,
|
||||
// and these hooks will not be evaluated.
|
||||
//
|
||||
// Processing of hooks stop after the first one that returns true.
|
||||
// The returned why string of the first match is used in logging.
|
||||
// Returning false does not drop the packet.
|
||||
// See also [filter.Filter.IngressAllowHooks].
|
||||
IngressAllowHooks []PacketMatch
|
||||
|
||||
// LinkLocalAllowHooks are hooks that provide exceptions to the default
|
||||
// policy of dropping link-local unicast packets. They run inside the
|
||||
// direction-agnostic pre-checks for both ingress and egress.
|
||||
//
|
||||
// A hook can allow a link-local packet to pass the link-local check,
|
||||
// but the packet is still subject to all other filter rules, and could be
|
||||
// dropped elsewhere. Matching link-local packets are not logged.
|
||||
// See also [filter.Filter.LinkLocalAllowHooks].
|
||||
LinkLocalAllowHooks []PacketMatch
|
||||
}
|
||||
|
||||
// PacketMatch is a function that inspects a packet and reports whether it
|
||||
// matches a custom filter criterion. If match is true, why should be a short
|
||||
// human-readable reason for the match, used in filter logging (e.g. "corp-dns ok").
|
||||
type PacketMatch func(packet.Parsed) (match bool, why string)
|
||||
|
||||
// filterState is a state cache of past seen packets.
|
||||
type filterState struct {
|
||||
mu sync.Mutex
|
||||
@@ -426,6 +457,16 @@ func (f *Filter) RunIn(q *packet.Parsed, rf RunFlags) Response {
|
||||
default:
|
||||
r, why = Drop, "not-ip"
|
||||
}
|
||||
|
||||
if r == noVerdict {
|
||||
for _, pm := range f.IngressAllowHooks {
|
||||
if match, why := pm(*q); match {
|
||||
f.logRateLimit(rf, q, dir, Accept, why)
|
||||
return Accept
|
||||
}
|
||||
}
|
||||
r = Drop
|
||||
}
|
||||
f.logRateLimit(rf, q, dir, r, why)
|
||||
return r
|
||||
}
|
||||
@@ -439,6 +480,7 @@ func (f *Filter) RunOut(q *packet.Parsed, rf RunFlags) (Response, usermetric.Dro
|
||||
// already logged
|
||||
return r, reason
|
||||
}
|
||||
|
||||
r, why := f.runOut(q)
|
||||
f.logRateLimit(rf, q, dir, r, why)
|
||||
return r, ""
|
||||
@@ -455,12 +497,14 @@ func unknownProtoString(proto ipproto.Proto) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// runIn4 returns noVerdict for unaccepted packets that may ultimately
|
||||
// be accepted through [Filter.IngressAllowHooks].
|
||||
func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
// A compromised peer could try to send us packets for
|
||||
// destinations we didn't explicitly advertise. This check is to
|
||||
// prevent that.
|
||||
if !f.local4(q.Dst.Addr()) {
|
||||
return Drop, "destination not allowed"
|
||||
return noVerdict, "destination not allowed"
|
||||
}
|
||||
|
||||
switch q.IPProto {
|
||||
@@ -510,17 +554,19 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
if f.matches4.matchProtoAndIPsOnlyIfAllPorts(q) {
|
||||
return Accept, "other-portless ok"
|
||||
}
|
||||
return Drop, unknownProtoString(q.IPProto)
|
||||
return noVerdict, unknownProtoString(q.IPProto)
|
||||
}
|
||||
return Drop, "no rules matched"
|
||||
return noVerdict, "no rules matched"
|
||||
}
|
||||
|
||||
// runIn6 returns noVerdict for unaccepted packets that may ultimately
|
||||
// be accepted through [Filter.IngressAllowHooks].
|
||||
func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
||||
// A compromised peer could try to send us packets for
|
||||
// destinations we didn't explicitly advertise. This check is to
|
||||
// prevent that.
|
||||
if !f.local6(q.Dst.Addr()) {
|
||||
return Drop, "destination not allowed"
|
||||
return noVerdict, "destination not allowed"
|
||||
}
|
||||
|
||||
switch q.IPProto {
|
||||
@@ -570,13 +616,30 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
||||
if f.matches6.matchProtoAndIPsOnlyIfAllPorts(q) {
|
||||
return Accept, "other-portless ok"
|
||||
}
|
||||
return Drop, unknownProtoString(q.IPProto)
|
||||
return noVerdict, unknownProtoString(q.IPProto)
|
||||
}
|
||||
return Drop, "no rules matched"
|
||||
return noVerdict, "no rules matched"
|
||||
}
|
||||
|
||||
// runIn runs the output-specific part of the filter logic.
|
||||
// runOut runs the output-specific part of the filter logic.
|
||||
func (f *Filter) runOut(q *packet.Parsed) (r Response, why string) {
|
||||
f.UpdateOutboundFlowState(q)
|
||||
return Accept, "ok out"
|
||||
}
|
||||
|
||||
// UpdateOutboundFlowState records reverse-flow connection-tracking state for
|
||||
// the given outbound packet so that subsequent inbound replies on the same
|
||||
// flow are admitted by [Filter.RunIn] without an explicit allow rule.
|
||||
//
|
||||
// Only UDP and SCTP packets are tracked; for other protocols this is a no-op.
|
||||
//
|
||||
// It is intended for callers that synthesize outbound packets and bypass
|
||||
// [Filter.RunOut] (for example netstack's [InjectOutbound] path used by
|
||||
// userspace networking, tsnet and the SOCKS5/HTTP proxies), so that reply
|
||||
// packets matching an outbound UDP flow are not silently dropped as "no
|
||||
// matching rule" by [Filter.RunIn]. See tailscale/tailscale#14229 and
|
||||
// tailscale/tailscale#20064.
|
||||
func (f *Filter) UpdateOutboundFlowState(q *packet.Parsed) {
|
||||
switch q.IPProto {
|
||||
case ipproto.UDP, ipproto.SCTP:
|
||||
tuple := flowtrack.MakeTuple(q.IPProto, q.Dst, q.Src) // src/dst reversed
|
||||
@@ -584,7 +647,6 @@ func (f *Filter) runOut(q *packet.Parsed) (r Response, why string) {
|
||||
f.state.lru.Add(tuple, struct{}{})
|
||||
f.state.mu.Unlock()
|
||||
}
|
||||
return Accept, "ok out"
|
||||
}
|
||||
|
||||
// direction is whether a packet was flowing into this machine, or
|
||||
@@ -609,6 +671,18 @@ func (d direction) String() string {
|
||||
|
||||
var gcpDNSAddr = netaddr.IPv4(169, 254, 169, 254)
|
||||
|
||||
func (f *Filter) isAllowedLinkLocal(q *packet.Parsed) bool {
|
||||
if q.Dst.Addr() == gcpDNSAddr {
|
||||
return true
|
||||
}
|
||||
for _, pm := range f.LinkLocalAllowHooks {
|
||||
if match, _ := pm(*q); match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pre runs the direction-agnostic filter logic. dir is only used for
|
||||
// logging.
|
||||
func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) (Response, usermetric.DropReason) {
|
||||
@@ -630,7 +704,7 @@ func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) (Response, us
|
||||
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
||||
return Drop, usermetric.ReasonMulticast
|
||||
}
|
||||
if q.Dst.Addr().IsLinkLocalUnicast() && q.Dst.Addr() != gcpDNSAddr {
|
||||
if q.Dst.Addr().IsLinkLocalUnicast() && !f.isAllowedLinkLocal(q) {
|
||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||
return Drop, usermetric.ReasonLinkLocalUnicast
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package filtertype defines the types used by wgengine/filter.
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/tailcfg/peercap"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
@@ -56,7 +58,7 @@ type CapMatch struct {
|
||||
|
||||
// Cap is the capability that's granted if the destination IP addresses
|
||||
// matches Dst.
|
||||
Cap tailcfg.PeerCapability
|
||||
Cap peercap.Cap
|
||||
|
||||
// Values are the raw JSON values of the capability.
|
||||
// See tailcfg.PeerCapability and tailcfg.PeerCapMap for details.
|
||||
@@ -84,7 +86,7 @@ type Match struct {
|
||||
// has one of these capabilities, it's also permitted. The peers are only
|
||||
// looked up by their self address (Node.Addresses) and not by subnet routes
|
||||
// they advertise.
|
||||
SrcCaps []tailcfg.NodeCapability
|
||||
SrcCaps []nodecap.Cap
|
||||
|
||||
Dsts []NetPortRange // optional, if source matches
|
||||
Caps []CapMatch // optional, if source match
|
||||
|
||||
+5
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/tailcfg/peercap"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
@@ -39,7 +41,7 @@ var _MatchCloneNeedsRegeneration = Match(struct {
|
||||
IPProto views.Slice[ipproto.Proto]
|
||||
Srcs []netip.Prefix
|
||||
SrcsContains func(netip.Addr) bool
|
||||
SrcCaps []tailcfg.NodeCapability
|
||||
SrcCaps []nodecap.Cap
|
||||
Dsts []NetPortRange
|
||||
Caps []CapMatch
|
||||
}{})
|
||||
@@ -59,6 +61,6 @@ func (src *CapMatch) Clone() *CapMatch {
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _CapMatchCloneNeedsRegeneration = CapMatch(struct {
|
||||
Dst netip.Prefix
|
||||
Cap tailcfg.PeerCapability
|
||||
Cap peercap.Cap
|
||||
Values []tailcfg.RawMessage
|
||||
}{})
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package filter
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/wgengine/filter/filtertype"
|
||||
)
|
||||
@@ -56,8 +56,8 @@ func srcMatches(m *filtertype.Match, srcAddr netip.Addr, hasCap CapTestFunc) boo
|
||||
// CapTestFunc is the function signature of a function that tests whether srcIP
|
||||
// has a given capability.
|
||||
//
|
||||
// It it used in the fast path of evaluating filter rules so should be fast.
|
||||
type CapTestFunc = func(srcIP netip.Addr, cap tailcfg.NodeCapability) bool
|
||||
// It is used in the fast path of evaluating filter rules so should be fast.
|
||||
type CapTestFunc = func(srcIP netip.Addr, cap nodecap.Cap) bool
|
||||
|
||||
func (ms matches) matchIPsOnly(q *packet.Parsed, hasCap CapTestFunc) bool {
|
||||
srcAddr := q.Src.Addr()
|
||||
|
||||
+7
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package filter
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"tailscale.com/net/ipset"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tailcfg/nodecap"
|
||||
"tailscale.com/tailcfg/peercap"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
@@ -104,7 +106,7 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
|
||||
for cap, val := range cm.CapMap {
|
||||
m.Caps = append(m.Caps, CapMatch{
|
||||
Dst: dstNet,
|
||||
Cap: tailcfg.PeerCapability(cap),
|
||||
Cap: peercap.Cap(cap),
|
||||
Values: val,
|
||||
})
|
||||
}
|
||||
@@ -131,9 +133,9 @@ var (
|
||||
//
|
||||
// TODO(bradfitz): make this return an IPSet and plumb that all
|
||||
// around, and ultimately use a new version of IPSet.ContainsFunc like
|
||||
// Contains16Func that works in [16]byte address, so we we can match
|
||||
// Contains16Func that works in [16]byte address, so we can match
|
||||
// at runtime without allocating?
|
||||
func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap tailcfg.NodeCapability, err error) {
|
||||
func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap nodecap.Cap, err error) {
|
||||
if arg == "*" {
|
||||
// User explicitly requested wildcard.
|
||||
return []netip.Prefix{
|
||||
@@ -142,7 +144,7 @@ func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap tailcfg.NodeCapabi
|
||||
}, "", nil
|
||||
}
|
||||
if cap, ok := strings.CutPrefix(arg, "cap:"); ok {
|
||||
return nil, tailcfg.NodeCapability(cap), nil
|
||||
return nil, nodecap.Cap(cap), nil
|
||||
}
|
||||
if strings.Contains(arg, "/") {
|
||||
pfx, err := netip.ParsePrefix(arg)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -42,7 +42,7 @@ func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
|
||||
if c.derpMap != nil {
|
||||
type D struct {
|
||||
regionID int
|
||||
regionID tailcfg.DERPRegionID
|
||||
lastWrite time.Time
|
||||
createTime time.Time
|
||||
}
|
||||
@@ -108,8 +108,8 @@ func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
|
||||
|
||||
peers := map[key.NodePublic]tailcfg.NodeView{}
|
||||
for _, p := range c.peers.All() {
|
||||
peers := make(map[key.NodePublic]tailcfg.NodeView, len(c.peersByID))
|
||||
for _, p := range c.peersByID {
|
||||
peers[p.Key()] = p
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !js
|
||||
@@ -62,7 +62,8 @@ var (
|
||||
//
|
||||
//lint:ignore U1000 used on Linux/Darwin only
|
||||
debugPMTUD = envknob.RegisterBool("TS_DEBUG_PMTUD")
|
||||
// debugNeverDirectUDP disables the use of direct UDP connections, forcing
|
||||
// debugNeverDirectUDP disables the use of direct UDP connections by
|
||||
// suppressing/dropping inbound/outbound [disco.Ping] messages, forcing
|
||||
// all peer communication over DERP or peer relay.
|
||||
debugNeverDirectUDP = envknob.RegisterBool("TS_DEBUG_NEVER_DIRECT_UDP")
|
||||
// Hey you! Adding a new debugknob? Make sure to stub it out in the
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios || js
|
||||
|
||||
+105
-35
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -6,6 +6,7 @@ package magicsock
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
@@ -44,12 +45,12 @@ const frameReceiveRecordRate = 5 * time.Second
|
||||
// current connection for that regionID is dc. (but dc should not be
|
||||
// used to write directly; it's owned by the read/write loops)
|
||||
type derpRoute struct {
|
||||
regionID int
|
||||
regionID tailcfg.DERPRegionID
|
||||
dc *derphttp.Client // don't use directly; see comment above
|
||||
}
|
||||
|
||||
// removeDerpPeerRoute removes a DERP route entry previously added by addDerpPeerRoute.
|
||||
func (c *Conn) removeDerpPeerRoute(peer key.NodePublic, regionID int, dc *derphttp.Client) {
|
||||
func (c *Conn) removeDerpPeerRoute(peer key.NodePublic, regionID tailcfg.DERPRegionID, dc *derphttp.Client) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
r2 := derpRoute{regionID, dc}
|
||||
@@ -61,7 +62,7 @@ func (c *Conn) removeDerpPeerRoute(peer key.NodePublic, regionID int, dc *derpht
|
||||
// addDerpPeerRoute adds a DERP route entry, noting that peer was seen
|
||||
// on DERP node derpID, at least on the connection identified by dc.
|
||||
// See issue 150 for details.
|
||||
func (c *Conn) addDerpPeerRoute(peer key.NodePublic, regionID int, dc *derphttp.Client) {
|
||||
func (c *Conn) addDerpPeerRoute(peer key.NodePublic, regionID tailcfg.DERPRegionID, dc *derphttp.Client) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
mak.Set(&c.derpRoute, peer, derpRoute{regionID, dc})
|
||||
@@ -78,7 +79,7 @@ func (c *Conn) addDerpPeerRoute(peer key.NodePublic, regionID int, dc *derphttp.
|
||||
// first connection.
|
||||
//
|
||||
// This can also help nodes from a slow or misbehaving control plane.
|
||||
func (c *Conn) fallbackDERPRegionForPeer(peer key.NodePublic) (regionID int) {
|
||||
func (c *Conn) fallbackDERPRegionForPeer(peer key.NodePublic) (regionID tailcfg.DERPRegionID) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if dr, ok := c.derpRoute[peer]; ok {
|
||||
@@ -100,7 +101,8 @@ type activeDerp struct {
|
||||
}
|
||||
|
||||
var (
|
||||
pickDERPFallbackForTests func() int
|
||||
pickDERPFallbackForTests func() tailcfg.DERPRegionID
|
||||
reSTUNHookForTests func(why string)
|
||||
)
|
||||
|
||||
// pickDERPFallback returns a non-zero but deterministic DERP node to
|
||||
@@ -109,7 +111,7 @@ var (
|
||||
// checks aren't working).
|
||||
//
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) pickDERPFallback() int {
|
||||
func (c *Conn) pickDERPFallback() tailcfg.DERPRegionID {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -154,7 +156,7 @@ var checkControlHealthDuringNearestDERPInTests = false
|
||||
// region that it selected and set (via setNearestDERP).
|
||||
//
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int) {
|
||||
func (c *Conn) maybeSetNearestDERP(report *netcheck.Report, force bool) (preferredDERP tailcfg.DERPRegionID) {
|
||||
// Don't change our PreferredDERP if we don't have a connection to
|
||||
// control; if we don't, then we can't inform peers about a DERP home
|
||||
// change, which breaks all connectivity. Even if this DERP region is
|
||||
@@ -168,7 +170,10 @@ func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int)
|
||||
//
|
||||
// Despite the above behaviour, ensure that we set the nearest DERP if
|
||||
// we don't currently have one set; any DERP server is better than
|
||||
// none, even if not connected to control.
|
||||
// none, even if not connected to control. The exception here is if we have
|
||||
// a cached netmap with a previous DERP server. Retaining the previous DERP
|
||||
// makes it easier for other nodes to find each other when control is not
|
||||
// available.
|
||||
var connectedToControl bool
|
||||
if testenv.InTest() && !checkControlHealthDuringNearestDERPInTests {
|
||||
connectedToControl = true
|
||||
@@ -178,7 +183,7 @@ func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int)
|
||||
c.mu.Lock()
|
||||
myDerp := c.myDerp
|
||||
c.mu.Unlock()
|
||||
if !connectedToControl {
|
||||
if !connectedToControl && !force {
|
||||
if myDerp != 0 {
|
||||
metricDERPHomeNoChangeNoControl.Add(1)
|
||||
return myDerp
|
||||
@@ -197,16 +202,33 @@ func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int)
|
||||
}
|
||||
if preferredDERP != myDerp {
|
||||
c.logf(
|
||||
"magicsock: home DERP changing from derp-%d [%dms] to derp-%d [%dms]",
|
||||
c.myDerp, report.RegionLatency[myDerp].Milliseconds(), preferredDERP, report.RegionLatency[preferredDERP].Milliseconds())
|
||||
"magicsock: home DERP changing from derp-%d [%dms] to derp-%d [%dms] (forced=%t)",
|
||||
c.myDerp, report.RegionLatency[myDerp].Milliseconds(), preferredDERP, report.RegionLatency[preferredDERP].Milliseconds(), force)
|
||||
}
|
||||
if !c.setNearestDERP(preferredDERP) {
|
||||
preferredDERP = 0
|
||||
} else if preferredDERP != myDerp {
|
||||
c.homeDERPChangedPub.Publish(HomeDERPChanged{Old: myDerp, New: preferredDERP})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeLocked(regionID int) string {
|
||||
// HomeDERPChanged is an event sent on the [eventbus.Bus] when a new home DERP
|
||||
// server has been selected. Its publisher is [magicsock.Coon]; its main
|
||||
// subscriber is [ipnlocal.LocalBackend] that updates the homeDERP used by the
|
||||
// netmap cache.
|
||||
// TODO(cmol): Move the subscriber to not inject into localBackend, but rather
|
||||
// into the netmap at the controlClient mapSession level once there is a stable
|
||||
// abstraction to use.
|
||||
type HomeDERPChanged struct {
|
||||
Old, New tailcfg.DERPRegionID
|
||||
}
|
||||
|
||||
func (c *Conn) ForceSetNearestDERP(regionID tailcfg.DERPRegionID) tailcfg.DERPRegionID {
|
||||
return c.maybeSetNearestDERP(&netcheck.Report{PreferredDERP: regionID}, true)
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeLocked(regionID tailcfg.DERPRegionID) string {
|
||||
if c.derpMap == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -219,14 +241,14 @@ func (c *Conn) derpRegionCodeLocked(regionID int) string {
|
||||
// setHomeDERPGaugeLocked updates the home DERP gauge metric.
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *Conn) setHomeDERPGaugeLocked(derpNum int) {
|
||||
func (c *Conn) setHomeDERPGaugeLocked(derpNum tailcfg.DERPRegionID) {
|
||||
if c.homeDERPGauge != nil {
|
||||
c.homeDERPGauge.Set(float64(derpNum))
|
||||
}
|
||||
}
|
||||
|
||||
// c.mu must NOT be held.
|
||||
func (c *Conn) setNearestDERP(derpNum int) (wantDERP bool) {
|
||||
func (c *Conn) setNearestDERP(derpNum tailcfg.DERPRegionID) (wantDERP bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.wantDerpLocked() {
|
||||
@@ -284,7 +306,7 @@ func (c *Conn) startDerpHomeConnectLocked() {
|
||||
// DERP region ID.
|
||||
//
|
||||
// c.mu may be held, but does not need to be.
|
||||
func (c *Conn) goDerpConnect(regionID int) {
|
||||
func (c *Conn) goDerpConnect(regionID tailcfg.DERPRegionID) {
|
||||
if regionID == 0 {
|
||||
return
|
||||
}
|
||||
@@ -314,7 +336,7 @@ const derpWriteQueueDepth = 32
|
||||
//
|
||||
// It returns nil if the network is down, the Conn is closed, or the regionID is
|
||||
// not known.
|
||||
func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan derpWriteRequest {
|
||||
func (c *Conn) derpWriteChanForRegion(regionID tailcfg.DERPRegionID, peer key.NodePublic) chan derpWriteRequest {
|
||||
if c.networkDown() {
|
||||
return nil
|
||||
}
|
||||
@@ -368,8 +390,8 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
firstDerp := false
|
||||
if c.activeDerp == nil {
|
||||
firstDerp = true
|
||||
c.activeDerp = make(map[int]activeDerp)
|
||||
c.prevDerp = make(map[int]*syncs.WaitGroupChan)
|
||||
c.activeDerp = make(map[tailcfg.DERPRegionID]activeDerp)
|
||||
c.prevDerp = make(map[tailcfg.DERPRegionID]*syncs.WaitGroupChan)
|
||||
}
|
||||
|
||||
// Note that derphttp.NewRegionClient does not dial the server
|
||||
@@ -392,6 +414,10 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
return derpMap.Regions[regionID]
|
||||
})
|
||||
dc.HealthTracker = c.health
|
||||
dc.AppName = c.derpAppName
|
||||
if c.extraRootCAs != nil {
|
||||
dc.TLSConfig = &tls.Config{RootCAs: c.extraRootCAs}
|
||||
}
|
||||
|
||||
dc.SetCanAckPings(true)
|
||||
dc.NotePreferred(c.myDerp == regionID)
|
||||
@@ -436,7 +462,14 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
|
||||
go c.runDerpReader(ctx, regionID, dc, wg, startGate)
|
||||
go c.runDerpWriter(ctx, dc, ch, wg, startGate)
|
||||
go c.derpActiveFunc()
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-startGate:
|
||||
c.derpActiveFunc()
|
||||
}
|
||||
}()
|
||||
|
||||
return ad.writeCh
|
||||
}
|
||||
@@ -448,7 +481,7 @@ func (c *Conn) derpWriteChanForRegion(regionID int, peer key.NodePublic) chan de
|
||||
// If there's any change, it logs.
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *Conn) setPeerLastDerpLocked(peer key.NodePublic, regionID, homeID int) {
|
||||
func (c *Conn) setPeerLastDerpLocked(peer key.NodePublic, regionID, homeID tailcfg.DERPRegionID) {
|
||||
if peer.IsZero() {
|
||||
return
|
||||
}
|
||||
@@ -484,7 +517,7 @@ func (c *Conn) setPeerLastDerpLocked(peer key.NodePublic, regionID, homeID int)
|
||||
// get at the packet contents they need to call copyBuf to copy it
|
||||
// out, which also releases the buffer.
|
||||
type derpReadResult struct {
|
||||
regionID int
|
||||
regionID tailcfg.DERPRegionID
|
||||
n int // length of data received
|
||||
src key.NodePublic
|
||||
// copyBuf is called to copy the data to dst. It returns how
|
||||
@@ -497,7 +530,7 @@ type derpReadResult struct {
|
||||
|
||||
// runDerpReader runs in a goroutine for the life of a DERP
|
||||
// connection, handling received packets.
|
||||
func (c *Conn) runDerpReader(ctx context.Context, regionID int, dc *derphttp.Client, wg *syncs.WaitGroupChan, startGate <-chan struct{}) {
|
||||
func (c *Conn) runDerpReader(ctx context.Context, regionID tailcfg.DERPRegionID, dc *derphttp.Client, wg *syncs.WaitGroupChan, startGate <-chan struct{}) {
|
||||
defer wg.Decr()
|
||||
defer dc.Close()
|
||||
|
||||
@@ -675,7 +708,7 @@ func (c *Conn) runDerpWriter(ctx context.Context, dc *derphttp.Client, ch <-chan
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connBind) receiveDERP(buffs [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
|
||||
func (c *connBind) receiveDERP(slab []byte, packets []conn.ReceivedPacket) (int, error) {
|
||||
if s := c.Conn.health.ReceiveFuncStats(health.ReceiveDERP); s != nil {
|
||||
s.Enter()
|
||||
defer s.Exit()
|
||||
@@ -685,13 +718,14 @@ func (c *connBind) receiveDERP(buffs [][]byte, sizes []int, eps []conn.Endpoint)
|
||||
if c.isClosed() {
|
||||
break
|
||||
}
|
||||
n, ep := c.processDERPReadResult(dm, buffs[0])
|
||||
n, ep := c.processDERPReadResult(dm, slab)
|
||||
if n == 0 {
|
||||
// No data read occurred. Wait for another packet.
|
||||
continue
|
||||
}
|
||||
sizes[0] = n
|
||||
eps[0] = ep
|
||||
packets[0].Size = n
|
||||
packets[0].Offset = 0
|
||||
packets[0].Endpoint = ep
|
||||
return 1, nil
|
||||
}
|
||||
return 0, net.ErrClosed
|
||||
@@ -701,7 +735,7 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
|
||||
if dm.copyBuf == nil {
|
||||
return 0, nil
|
||||
}
|
||||
var regionID int
|
||||
var regionID tailcfg.DERPRegionID
|
||||
n, regionID = dm.n, dm.regionID
|
||||
ncopy := dm.copyBuf(b)
|
||||
if ncopy != n {
|
||||
@@ -718,6 +752,10 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if c.onDERPRecv != nil && c.onDERPRecv(regionID, dm.src, b[:n]) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var ok bool
|
||||
c.mu.Lock()
|
||||
ep, ok = c.peerMap.endpointForNodeKey(dm.src)
|
||||
@@ -738,6 +776,15 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
|
||||
return n, ep
|
||||
}
|
||||
|
||||
// SendDERPPacketTo sends an arbitrary packet to the given node key via
|
||||
// the DERP relay for the given region. It creates the DERP connection
|
||||
// to the region if one doesn't already exist.
|
||||
func (c *Conn) SendDERPPacketTo(dstKey key.NodePublic, regionID tailcfg.DERPRegionID, pkt []byte) (sent bool, err error) {
|
||||
return c.sendAddr(
|
||||
netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(regionID)),
|
||||
dstKey, pkt, false, false)
|
||||
}
|
||||
|
||||
// SetOnlyTCP443 set whether the magicsock connection is restricted
|
||||
// to only using TCP port 443 outbound. If true, no UDP is allowed,
|
||||
// no STUN checks are performend, etc.
|
||||
@@ -747,7 +794,24 @@ func (c *Conn) SetOnlyTCP443(v bool) {
|
||||
|
||||
// SetDERPMap controls which (if any) DERP servers are used.
|
||||
// A nil value means to disable DERP; it's disabled by default.
|
||||
//
|
||||
// SetDERPMap triggers a ReSTUN after updating the map. Callers that want to
|
||||
// set the map without triggering a ReSTUN should use [Conn.SetDERPMapWithoutReSTUN]
|
||||
// instead.
|
||||
func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
c.setDERPMap(dm, true)
|
||||
}
|
||||
|
||||
// SetDERPMapWithoutReSTUN is like [Conn.SetDERPMap] but does not trigger a
|
||||
// ReSTUN after updating the map.
|
||||
//
|
||||
// It is used for setting the map from a cache, so the homeDERP can be set
|
||||
// from cache before any STUN happens.
|
||||
func (c *Conn) SetDERPMapWithoutReSTUN(dm *tailcfg.DERPMap) {
|
||||
c.setDERPMap(dm, false)
|
||||
}
|
||||
|
||||
func (c *Conn) setDERPMap(dm *tailcfg.DERPMap, doReStun bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -760,7 +824,7 @@ func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
}
|
||||
dm = &tailcfg.DERPMap{
|
||||
OmitDefaultRegions: true,
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
Regions: map[tailcfg.DERPRegionID]*tailcfg.DERPRegion{
|
||||
999: {
|
||||
RegionID: 999,
|
||||
Nodes: []*tailcfg.DERPNode{{
|
||||
@@ -804,8 +868,14 @@ func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
}
|
||||
}
|
||||
|
||||
go c.ReSTUN("derp-map-update")
|
||||
if doReStun {
|
||||
if reSTUNHookForTests != nil {
|
||||
reSTUNHookForTests("derp-map-update")
|
||||
}
|
||||
go c.ReSTUN("derp-map-update")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) wantDerpLocked() bool { return c.derpMap != nil }
|
||||
|
||||
// c.mu must be held.
|
||||
@@ -871,7 +941,7 @@ func (c *Conn) maybeCloseDERPsOnRebind(okayLocalIPs []netip.Prefix) {
|
||||
// why is a reason for logging.
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *Conn) closeOrReconnectDERPLocked(regionID int, why string) {
|
||||
func (c *Conn) closeOrReconnectDERPLocked(regionID tailcfg.DERPRegionID, why string) {
|
||||
c.closeDerpLocked(regionID, why)
|
||||
if !c.privateKey.IsZero() && c.myDerp == regionID {
|
||||
c.startDerpHomeConnectLocked()
|
||||
@@ -880,7 +950,7 @@ func (c *Conn) closeOrReconnectDERPLocked(regionID int, why string) {
|
||||
|
||||
// c.mu must be held.
|
||||
// It is the responsibility of the caller to call logActiveDerpLocked after any set of closes.
|
||||
func (c *Conn) closeDerpLocked(regionID int, why string) {
|
||||
func (c *Conn) closeDerpLocked(regionID tailcfg.DERPRegionID, why string) {
|
||||
if ad, ok := c.activeDerp[regionID]; ok {
|
||||
c.logf("magicsock: closing connection to derp-%v (%v), age %v", regionID, why, time.Since(ad.createTime).Round(time.Second))
|
||||
go ad.c.Close()
|
||||
@@ -898,14 +968,14 @@ func (c *Conn) logActiveDerpLocked() {
|
||||
return
|
||||
}
|
||||
buf.WriteString(":")
|
||||
c.foreachActiveDerpSortedLocked(func(node int, ad activeDerp) {
|
||||
c.foreachActiveDerpSortedLocked(func(node tailcfg.DERPRegionID, ad activeDerp) {
|
||||
fmt.Fprintf(buf, " derp-%d=cr%v,wr%v", node, simpleDur(now.Sub(ad.createTime)), simpleDur(now.Sub(*ad.lastWrite)))
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// c.mu must be held.
|
||||
func (c *Conn) foreachActiveDerpSortedLocked(fn func(regionID int, ad activeDerp)) {
|
||||
func (c *Conn) foreachActiveDerpSortedLocked(fn func(regionID tailcfg.DERPRegionID, ad activeDerp)) {
|
||||
if len(c.activeDerp) < 2 {
|
||||
for id, ad := range c.activeDerp {
|
||||
fn(id, ad)
|
||||
@@ -971,7 +1041,7 @@ func (c *Conn) DERPs() int {
|
||||
return len(c.activeDerp)
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeOfIDLocked(regionID int) string {
|
||||
func (c *Conn) derpRegionCodeOfIDLocked(regionID tailcfg.DERPRegionID) string {
|
||||
if c.derpMap == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by "stringer -type=discoPingPurpose -trimprefix=ping"; DO NOT EDIT.
|
||||
|
||||
+304
-65
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/stun"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/bools"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -70,7 +72,12 @@ type endpoint struct {
|
||||
fakeWGAddr netip.AddrPort // the UDP address we tell wireguard-go we're using
|
||||
nodeAddr netip.Addr // the node's first tailscale address; used for logging & wireguard rate-limiting (Issue 6686)
|
||||
|
||||
disco atomic.Pointer[endpointDisco] // if the peer supports disco, the key and short string
|
||||
// disco is non-nil if the peer supports disco, and in that case holds
|
||||
// the peer's disco key and short string.
|
||||
// It is safe to load and read the contents of the value, but the caller
|
||||
// must not store directly into this field. Instead, use
|
||||
// [updateDiscoKey] and [updateTSMPDiscoKey].
|
||||
disco atomic.Pointer[endpointDisco]
|
||||
|
||||
// mu protects all following fields.
|
||||
mu syncs.Mutex // Lock ordering: Conn.mu, then endpoint.mu
|
||||
@@ -96,7 +103,7 @@ type endpoint struct {
|
||||
probeUDPLifetime *probeUDPLifetime // UDP path lifetime probing; nil if disabled
|
||||
|
||||
expired bool // whether the node has expired
|
||||
isWireguardOnly bool // whether the endpoint is WireGuard only
|
||||
isWireguardOnly bool // whether the endpoint is WireGuard only. Must not be changed after initializing the endpont.
|
||||
relayCapable bool // whether the node is capable of speaking via a [tailscale.com/net/udprelay.Server]
|
||||
}
|
||||
|
||||
@@ -132,6 +139,20 @@ func (de *endpoint) udpRelayEndpointReady(maybeBest addrQuality) {
|
||||
func (de *endpoint) setBestAddrLocked(v addrQuality) {
|
||||
if v.epAddr != de.bestAddr.epAddr {
|
||||
de.probeUDPLifetime.resetCycleEndpointLocked()
|
||||
|
||||
// Reaching here, if we are upgrading from an invalid (missing) address
|
||||
// to a valid one, record metrics:
|
||||
if !de.bestAddr.ap.IsValid() && v.ap.IsValid() {
|
||||
// If we are using data from a cached netmap, increment the counter for peers established.
|
||||
isCached := de.c.usingCachedNetmap.Load()
|
||||
if isCached {
|
||||
metricCachedPeerContactDirect.Add(1)
|
||||
}
|
||||
// Regardless whether the netmap is cached, record how long it has
|
||||
// been since the endpoint was initialized.
|
||||
de.c.logf("magicsock: new contact: peer=%s usec=%d cached=%v via=direct",
|
||||
de.publicKey.ShortString(), int64(mono.Since(de.c.initializedAt)/time.Microsecond), isCached)
|
||||
}
|
||||
}
|
||||
de.bestAddr = v
|
||||
}
|
||||
@@ -357,11 +378,64 @@ func (de *endpoint) setProbeUDPLifetimeConfigLocked(desired *ProbeUDPLifetimeCon
|
||||
p.resetCycleEndpointLocked()
|
||||
}
|
||||
|
||||
// endpointDisco is the current disco key and short string for an endpoint. This
|
||||
// structure is immutable.
|
||||
// endpointDisco is the current disco key and short string for an endpoint for
|
||||
// keys learned both from controlClient and via TSMP. Only one key is active at
|
||||
// a time for sending.
|
||||
//
|
||||
// This structure is immutable.
|
||||
type endpointDisco struct {
|
||||
key key.DiscoPublic // for discovery messages.
|
||||
short string // ShortString of discoKey.
|
||||
controlKey key.DiscoPublic // key learned via control for disco messages.
|
||||
tsmpKey key.DiscoPublic // key learned via TSMP for disco messages.
|
||||
controlShort string // ShortString of control learned key.
|
||||
tsmpShort string // ShortString of TSMP learned key.
|
||||
tsmpActive bool
|
||||
}
|
||||
|
||||
// key returns the disco key currently regarded as active or a zero key if
|
||||
// endpointDisco is nil.
|
||||
func (e *endpointDisco) key() key.DiscoPublic {
|
||||
if e == nil {
|
||||
return key.DiscoPublic{}
|
||||
}
|
||||
if e.tsmpActive {
|
||||
return e.tsmpKey
|
||||
}
|
||||
return e.controlKey
|
||||
}
|
||||
|
||||
func (e *endpointDisco) keyFromControl() key.DiscoPublic {
|
||||
if e == nil {
|
||||
return key.DiscoPublic{}
|
||||
}
|
||||
return e.controlKey
|
||||
}
|
||||
|
||||
func (e *endpointDisco) keyFromTSMP() key.DiscoPublic {
|
||||
if e == nil {
|
||||
return key.DiscoPublic{}
|
||||
}
|
||||
return e.tsmpKey
|
||||
}
|
||||
|
||||
// tsmpIsActive reports whether the TSMP learned key is currently the active
|
||||
// disco key. Safe to call on a nil receiver (returns false).
|
||||
func (e *endpointDisco) tsmpIsActive() bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
return e.tsmpActive
|
||||
}
|
||||
|
||||
// shortString returns the ShortString of the key currently regarded as active
|
||||
// or an empty string if endpointDisco is nil.
|
||||
func (e *endpointDisco) shortString() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.tsmpActive {
|
||||
return e.tsmpShort
|
||||
}
|
||||
return e.controlShort
|
||||
}
|
||||
|
||||
type sentPing struct {
|
||||
@@ -503,7 +577,9 @@ func (de *endpoint) initFakeUDPAddr() {
|
||||
// Conn.noteRecvActivity no more than once every 10s, returning true if it
|
||||
// was called, otherwise false.
|
||||
func (de *endpoint) noteRecvActivity(src epAddr, now mono.Time) bool {
|
||||
if de.isWireguardOnly {
|
||||
if !hasUDPTransport {
|
||||
// Nothing to track; src is always our DERP home.
|
||||
} else if de.isWireguardOnly {
|
||||
de.mu.Lock()
|
||||
de.bestAddr.ap = src.ap
|
||||
de.bestAddrAt = now
|
||||
@@ -524,22 +600,13 @@ func (de *endpoint) noteRecvActivity(src epAddr, now mono.Time) bool {
|
||||
elapsed := now.Sub(de.lastRecvWG.LoadAtomic())
|
||||
if elapsed > 10*time.Second {
|
||||
de.lastRecvWG.StoreAtomic(now)
|
||||
|
||||
if de.c.noteRecvActivity == nil {
|
||||
return false
|
||||
}
|
||||
de.c.noteRecvActivity(de.publicKey)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (de *endpoint) discoShort() string {
|
||||
var short string
|
||||
if d := de.disco.Load(); d != nil {
|
||||
short = d.short
|
||||
}
|
||||
return short
|
||||
return de.disco.Load().shortString()
|
||||
}
|
||||
|
||||
// String exists purely so wireguard-go internals can log.Printf("%v")
|
||||
@@ -572,7 +639,7 @@ func (de *endpoint) addrForSendLocked(now mono.Time) (udpAddr epAddr, derpAddr n
|
||||
return udpAddr, netip.AddrPort{}, false
|
||||
}
|
||||
|
||||
if de.isWireguardOnly {
|
||||
if hasUDPTransport && de.isWireguardOnly {
|
||||
// If the endpoint is wireguard-only, we don't have a DERP
|
||||
// address to send to, so we have to send to the UDP address.
|
||||
udpAddr, shouldPing := de.addrForWireGuardSendLocked(now)
|
||||
@@ -697,7 +764,7 @@ func (de *endpoint) maybeProbeUDPLifetimeLocked() (afterInactivityFor time.Durat
|
||||
// shuffling probing probability where the local node ends up with a large
|
||||
// key value lexicographically relative to the other nodes it tends to
|
||||
// communicate with. If de's disco key changes, the cycle will reset.
|
||||
if de.c.discoAtomic.Public().Compare(epDisco.key) >= 0 {
|
||||
if de.c.discoAtomic.Public().Compare(epDisco.key()) >= 0 {
|
||||
// lower disco pub key node probes higher
|
||||
return afterInactivityFor, false
|
||||
}
|
||||
@@ -826,6 +893,9 @@ func (de *endpoint) heartbeat() {
|
||||
if now.Sub(de.lastSendExt) > sessionActiveTimeout {
|
||||
// Session's idle. Stop heartbeating.
|
||||
de.c.dlogf("[v1] magicsock: disco: ending heartbeats for idle session to %v (%v)", de.publicKey.ShortString(), de.discoShort())
|
||||
if !buildfeatures.HasNATTraversal {
|
||||
return
|
||||
}
|
||||
if afterInactivityFor, ok := de.maybeProbeUDPLifetimeLocked(); ok {
|
||||
// This is the best place to best effort schedule a probe of UDP
|
||||
// path lifetime in the future as it loosely translates to "UDP path
|
||||
@@ -863,7 +933,7 @@ func (de *endpoint) heartbeat() {
|
||||
de.sendDiscoPingsLocked(now, true)
|
||||
}
|
||||
|
||||
if de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
if buildfeatures.HasNATTraversal && de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
de.discoverUDPRelayPathsLocked(now)
|
||||
}
|
||||
|
||||
@@ -891,7 +961,7 @@ func (de *endpoint) wantUDPRelayPathDiscoveryLocked(now mono.Time) bool {
|
||||
if runtime.GOOS == "js" {
|
||||
return false
|
||||
}
|
||||
if !de.c.hasPeerRelayServers.Load() {
|
||||
if !de.c.relayManager.hasPeerRelayServers.Load() {
|
||||
// Changes in this value between its access and a call to
|
||||
// [endpoint.discoverUDPRelayPathsLocked] are fine, we will eventually
|
||||
// do the "right" thing during future path discovery. The worst case is
|
||||
@@ -949,7 +1019,7 @@ func (de *endpoint) wantFullPingLocked(now mono.Time) bool {
|
||||
|
||||
func (de *endpoint) noteTxActivityExtTriggerLocked(now mono.Time) {
|
||||
de.lastSendExt = now
|
||||
if de.heartBeatTimer == nil && !de.heartbeatDisabled {
|
||||
if hasUDPTransport && de.heartBeatTimer == nil && !de.heartbeatDisabled {
|
||||
de.heartBeatTimer = time.AfterFunc(heartbeatInterval, de.heartbeat)
|
||||
}
|
||||
}
|
||||
@@ -1022,7 +1092,7 @@ func (de *endpoint) discoPing(res *ipnstate.PingResult, size int, cb func(*ipnst
|
||||
for ep := range de.endpointState {
|
||||
de.startDiscoPingLocked(epAddr{ap: ep}, now, pingCLI, size, resCB)
|
||||
}
|
||||
if de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
if buildfeatures.HasNATTraversal && de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
de.discoverUDPRelayPathsLocked(now)
|
||||
}
|
||||
}
|
||||
@@ -1044,13 +1114,15 @@ func (de *endpoint) send(buffs [][]byte, offset int) error {
|
||||
now := mono.Now()
|
||||
udpAddr, derpAddr, startWGPing := de.addrForSendLocked(now)
|
||||
|
||||
if de.isWireguardOnly {
|
||||
if !hasUDPTransport {
|
||||
// There are no UDP paths to discover; everything goes over DERP.
|
||||
} else if de.isWireguardOnly {
|
||||
if startWGPing {
|
||||
de.sendWireGuardOnlyPingsLocked(now)
|
||||
}
|
||||
} else if !udpAddr.isDirect() || now.After(de.trustBestAddrUntil) {
|
||||
de.sendDiscoPingsLocked(now, true)
|
||||
if de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
if buildfeatures.HasNATTraversal && de.wantUDPRelayPathDiscoveryLocked(now) {
|
||||
de.discoverUDPRelayPathsLocked(now)
|
||||
}
|
||||
}
|
||||
@@ -1070,7 +1142,7 @@ func (de *endpoint) send(buffs [][]byte, offset int) error {
|
||||
}
|
||||
}
|
||||
var err error
|
||||
if udpAddr.ap.IsValid() {
|
||||
if hasUDPTransport && udpAddr.ap.IsValid() {
|
||||
_, err = de.c.sendUDPBatch(udpAddr, buffs, offset)
|
||||
|
||||
// If the error is known to indicate that the endpoint is no longer
|
||||
@@ -1177,8 +1249,7 @@ func (de *endpoint) discoPingTimeout(txid stun.TxID) {
|
||||
return
|
||||
}
|
||||
bestUntrusted := mono.Now().After(de.trustBestAddrUntil)
|
||||
if sp.to == de.bestAddr.epAddr && sp.to.vni.IsSet() && bestUntrusted {
|
||||
// TODO(jwhited): consider applying this to direct UDP paths as well
|
||||
if sp.to == de.bestAddr.epAddr && bestUntrusted {
|
||||
de.clearBestAddrLocked()
|
||||
}
|
||||
if debugDisco() || !de.bestAddr.ap.IsValid() || bestUntrusted {
|
||||
@@ -1211,7 +1282,7 @@ func (de *endpoint) removeSentDiscoPingLocked(txid stun.TxID, sp sentPing, resul
|
||||
// Stop the timer for the case where sendPing failed to write to UDP.
|
||||
// In the case of a timer already having fired, this is a no-op:
|
||||
sp.timer.Stop()
|
||||
if sp.purpose == pingHeartbeatForUDPLifetime {
|
||||
if buildfeatures.HasNATTraversal && sp.purpose == pingHeartbeatForUDPLifetime {
|
||||
de.probeUDPLifetimeCliffDoneLocked(result, txid)
|
||||
}
|
||||
delete(de.sentPing, txid)
|
||||
@@ -1343,13 +1414,16 @@ func (de *endpoint) startDiscoPingLocked(ep epAddr, now mono.Time, purpose disco
|
||||
if purpose == pingHeartbeatForUDPLifetime && de.probeUDPLifetime != nil {
|
||||
de.probeUDPLifetime.lastTxID = txid
|
||||
}
|
||||
go de.sendDiscoPing(ep, epDisco.key, txid, s, logLevel)
|
||||
go de.sendDiscoPing(ep, epDisco.key(), txid, s, logLevel)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sendDiscoPingsLocked starts pinging all of ep's endpoints.
|
||||
// sendDiscoPingsLocked starts pinging all of ep's direct endpoints.
|
||||
// Sibling of discoverUDPRelayPathsLocked for udprelay.
|
||||
func (de *endpoint) sendDiscoPingsLocked(now mono.Time, sendCallMeMaybe bool) {
|
||||
if debugNeverDirectUDP() {
|
||||
return // skip when direct UDP is disabled
|
||||
}
|
||||
de.lastFullPing = now
|
||||
var sentAny bool
|
||||
for ep, st := range de.endpointState {
|
||||
@@ -1374,7 +1448,7 @@ func (de *endpoint) sendDiscoPingsLocked(now mono.Time, sendCallMeMaybe bool) {
|
||||
de.startDiscoPingLocked(epAddr{ap: ep}, now, pingDiscovery, 0, nil)
|
||||
}
|
||||
derpAddr := de.derpAddr
|
||||
if sentAny && sendCallMeMaybe && derpAddr.IsValid() {
|
||||
if buildfeatures.HasNATTraversal && sentAny && sendCallMeMaybe && derpAddr.IsValid() {
|
||||
// Have our magicsock.Conn figure out its STUN endpoint (if
|
||||
// it doesn't know already) and then send a CallMeMaybe
|
||||
// message to our peer via DERP informing them that we've
|
||||
@@ -1388,7 +1462,7 @@ func (de *endpoint) sendDiscoPingsLocked(now mono.Time, sendCallMeMaybe bool) {
|
||||
// a WireGuard only endpoint and initiates an ICMP ping for useable
|
||||
// addresses.
|
||||
func (de *endpoint) sendWireGuardOnlyPingsLocked(now mono.Time) {
|
||||
if runtime.GOOS == "js" {
|
||||
if !hasUDPTransport {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1464,8 +1538,167 @@ func (de *endpoint) setLastPing(ipp netip.AddrPort, now mono.Time) {
|
||||
state.lastPing = now
|
||||
}
|
||||
|
||||
// updateDiscoKey replaces the controlClient learned disco key for de.
|
||||
// Update only the control-provided key, leaving any existing TSMP key as-is.
|
||||
// If the new control key is zero, switch back to using the TSMP key if it
|
||||
// exists; otherwise mark the new control key as preferred.
|
||||
// Should both keys be zero, nil out the saved key.
|
||||
// The loop here ensures another update did not occur during the interval
|
||||
// between load and store (based on pointer identity).
|
||||
//
|
||||
// It reports whether the active disco key changed. If it reports true, the
|
||||
// caller must call [changeActiveDIscoLocked] to reset the state of the
|
||||
// disco connection.
|
||||
func (de *endpoint) updateDiscoKey(key key.DiscoPublic) bool {
|
||||
epDisco := &endpointDisco{}
|
||||
for {
|
||||
old := de.disco.Load()
|
||||
// Nothing to update
|
||||
if old == nil && key.IsZero() {
|
||||
return false
|
||||
}
|
||||
// Control key did not change and control is already active
|
||||
if old.keyFromControl() == key && !old.tsmpIsActive() && !key.IsZero() {
|
||||
return false
|
||||
}
|
||||
if old != nil {
|
||||
epDisco.tsmpKey = old.tsmpKey
|
||||
epDisco.tsmpShort = old.tsmpShort
|
||||
epDisco.tsmpActive = old.tsmpActive || key.IsZero()
|
||||
}
|
||||
if !key.IsZero() {
|
||||
epDisco.controlKey = key
|
||||
epDisco.controlShort = key.ShortString()
|
||||
}
|
||||
// We have no key material, nil out key.
|
||||
if epDisco.controlKey.IsZero() && epDisco.tsmpKey.IsZero() {
|
||||
epDisco = nil
|
||||
}
|
||||
if de.disco.CompareAndSwap(old, epDisco) {
|
||||
de.logKeyChange(old, epDisco, "controlClientUpdate")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateTSMPDiscoKey replaces the TSMP learned disco key for de.
|
||||
// Update only the TSMP-provided key, leaving any existing control key as-is.
|
||||
// If the new TSMP key is zero, switch back to using the control key if it
|
||||
// exists; otherwise mark the new TSMP key as preferred.
|
||||
// Should both keys be zero, nil out the saved key.
|
||||
// The loop here ensures another update did not occur during the interval
|
||||
// between load and store (based on pointer identity).
|
||||
//
|
||||
// Returns wether the active was swapped to the TSMP key, or if the TSMP key
|
||||
// changed. If the key has changed, call [changedActiveDiscoLocked] to reset
|
||||
// state of the disco connection.
|
||||
func (de *endpoint) updateTSMPDiscoKey(key key.DiscoPublic) bool {
|
||||
epDisco := &endpointDisco{}
|
||||
for {
|
||||
old := de.disco.Load()
|
||||
// Nothing to update
|
||||
if old == nil && key.IsZero() {
|
||||
return false
|
||||
}
|
||||
// TSMP key did not change and TSMP is already active
|
||||
if old.keyFromTSMP() == key && old.tsmpIsActive() && !key.IsZero() {
|
||||
return false
|
||||
}
|
||||
if old != nil {
|
||||
epDisco.controlKey = old.controlKey
|
||||
epDisco.controlShort = old.controlShort
|
||||
epDisco.tsmpActive = !key.IsZero()
|
||||
}
|
||||
if !key.IsZero() {
|
||||
epDisco.tsmpKey = key
|
||||
epDisco.tsmpShort = key.ShortString()
|
||||
epDisco.tsmpActive = true
|
||||
}
|
||||
// We have no key material, nil out key.
|
||||
if epDisco.controlKey.IsZero() && epDisco.tsmpKey.IsZero() {
|
||||
epDisco = nil
|
||||
}
|
||||
|
||||
// At this point, we know the relevant parts have changed enough to make an
|
||||
// update.
|
||||
if de.disco.CompareAndSwap(old, epDisco) {
|
||||
de.logKeyChange(old, epDisco, "tsmpUpdate")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (de *endpoint) logKeyChange(oldDisco, newDisco *endpointDisco, reason string) {
|
||||
de.c.logf("[v1] magicsock: disco: node %s changed key from %s to %s, tsmpActive=%t -> %t, reason=%s",
|
||||
de.publicKey.ShortString(), oldDisco.key(), newDisco.key(),
|
||||
oldDisco.tsmpIsActive(), newDisco.tsmpIsActive(), reason)
|
||||
}
|
||||
|
||||
// sawDiscoKeyTestHook is a test only hook to create a race on writing to
|
||||
// [disco]. It is never safe to use outside the contexts of tests.
|
||||
var sawDiscoKeyTestHook func()
|
||||
|
||||
// checkAndUpdateDiscoKey reports whether key is one of the known disco keys
|
||||
// for de. If so, and it's not already the active disco key for de, it is
|
||||
// made the active key. In either case, the resulting disco key bundle for
|
||||
// de is returned (it may be nil, if de has no disco keys).
|
||||
func (de *endpoint) checkAndUpdateDiscoKey(key key.DiscoPublic) (*endpointDisco, bool) {
|
||||
epDisco := &endpointDisco{}
|
||||
for {
|
||||
current := de.disco.Load()
|
||||
if current == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if !current.key().IsZero() && current.key() == key {
|
||||
return current, true
|
||||
}
|
||||
|
||||
inactiveKey := bools.IfElse(current.tsmpActive, current.keyFromControl(),
|
||||
current.keyFromTSMP())
|
||||
|
||||
// No reason to swap to a zero key or not the key we are seeing.
|
||||
if inactiveKey.IsZero() || inactiveKey != key {
|
||||
return current, false
|
||||
}
|
||||
|
||||
// Hook used for testing update conflict for the CompareAndSwap operation.
|
||||
// Only used in testing, not safe to use in production.
|
||||
if sawDiscoKeyTestHook != nil {
|
||||
sawDiscoKeyTestHook()
|
||||
}
|
||||
|
||||
// We have seen the inactive key, swap to that one as being active.
|
||||
epDisco.controlKey = current.controlKey
|
||||
epDisco.controlShort = current.controlShort
|
||||
epDisco.tsmpKey = current.tsmpKey
|
||||
epDisco.tsmpShort = current.tsmpShort
|
||||
epDisco.tsmpActive = !current.tsmpActive
|
||||
if de.disco.CompareAndSwap(current, epDisco) {
|
||||
de.mu.Lock()
|
||||
de.changedActiveDiscoLocked()
|
||||
de.mu.Unlock()
|
||||
de.logKeyChange(current, epDisco, "keySeen")
|
||||
return epDisco, true
|
||||
} else {
|
||||
// The struct changed under us, run the search again.
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// changedActiveDiscoLocked invalidates the current disco path for a re-probe,
|
||||
// but keeps the bestAddr so data keeps flowing until a new path is confirmed.
|
||||
//
|
||||
// de.mu must be held.
|
||||
func (de *endpoint) changedActiveDiscoLocked() {
|
||||
de.trustBestAddrUntil = 0
|
||||
de.invalidateDiscoPathLocked()
|
||||
}
|
||||
|
||||
// updateFromNode updates the endpoint based on a tailcfg.Node from a NetMap
|
||||
// update.
|
||||
// update. The node is assumed to originate from the control client from the
|
||||
// perspective of discoKey management.
|
||||
func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, probeUDPLifetimeEnabled bool) {
|
||||
if !n.Valid() {
|
||||
panic("nil node when updating endpoint")
|
||||
@@ -1474,30 +1707,28 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p
|
||||
defer de.mu.Unlock()
|
||||
|
||||
de.heartbeatDisabled = heartbeatDisabled
|
||||
if probeUDPLifetimeEnabled {
|
||||
de.setProbeUDPLifetimeConfigLocked(defaultProbeUDPLifetimeConfig)
|
||||
} else {
|
||||
de.setProbeUDPLifetimeConfigLocked(nil)
|
||||
if buildfeatures.HasNATTraversal {
|
||||
if probeUDPLifetimeEnabled {
|
||||
de.setProbeUDPLifetimeConfigLocked(defaultProbeUDPLifetimeConfig)
|
||||
} else {
|
||||
de.setProbeUDPLifetimeConfigLocked(nil)
|
||||
}
|
||||
}
|
||||
de.expired = n.Expired()
|
||||
|
||||
epDisco := de.disco.Load()
|
||||
var discoKey key.DiscoPublic
|
||||
if epDisco != nil {
|
||||
discoKey = epDisco.key
|
||||
}
|
||||
discoKey := de.disco.Load().keyFromControl()
|
||||
|
||||
if discoKey != n.DiscoKey() {
|
||||
de.c.logf("[v1] magicsock: disco: node %s changed from %s to %s", de.publicKey.ShortString(), discoKey, n.DiscoKey())
|
||||
de.disco.Store(&endpointDisco{
|
||||
key: n.DiscoKey(),
|
||||
short: n.DiscoKey().ShortString(),
|
||||
})
|
||||
de.c.logf("[v1] magicsock: disco: node %s changed control learned key from %s to %s",
|
||||
de.publicKey.ShortString(), discoKey, n.DiscoKey())
|
||||
key := n.DiscoKey()
|
||||
if de.updateDiscoKey(key) {
|
||||
de.changedActiveDiscoLocked()
|
||||
}
|
||||
de.debugUpdates.Add(EndpointChange{
|
||||
When: time.Now(),
|
||||
What: "updateFromNode-resetLocked",
|
||||
What: "updateFromNode-disco-key-changed",
|
||||
})
|
||||
de.resetLocked()
|
||||
}
|
||||
if n.HomeDERP() == 0 {
|
||||
if de.derpAddr.IsValid() {
|
||||
@@ -1521,7 +1752,9 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p
|
||||
de.derpAddr = newDerp
|
||||
}
|
||||
|
||||
de.setEndpointsLocked(n.Endpoints())
|
||||
if hasUDPTransport {
|
||||
de.setEndpointsLocked(n.Endpoints())
|
||||
}
|
||||
|
||||
de.relayCapable = capVerIsRelayCapable(n.Cap())
|
||||
}
|
||||
@@ -1719,7 +1952,7 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src epAdd
|
||||
now := mono.Now()
|
||||
latency := now.Sub(sp.at)
|
||||
|
||||
if !isDerp && !src.vni.IsSet() {
|
||||
if hasUDPTransport && !isDerp && !src.vni.IsSet() {
|
||||
// Note: we check vni.isSet() as relay [epAddr]'s are not stored in
|
||||
// endpointState, they are either de.bestAddr or not.
|
||||
st, ok := de.endpointState[sp.to.ap]
|
||||
@@ -1756,18 +1989,14 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src epAdd
|
||||
|
||||
// Promote this pong response to our current best address if it's lower latency.
|
||||
// TODO(bradfitz): decide how latency vs. preference order affects decision
|
||||
if !isDerp {
|
||||
if hasUDPTransport && !isDerp {
|
||||
thisPong := addrQuality{
|
||||
epAddr: sp.to,
|
||||
latency: latency,
|
||||
wireMTU: pingSizeToPktLen(sp.size, sp.to),
|
||||
}
|
||||
// TODO(jwhited): consider checking de.trustBestAddrUntil as well. If
|
||||
// de.bestAddr is untrusted we may want to clear it, otherwise we could
|
||||
// get stuck with a forever untrusted bestAddr that blackholes, since
|
||||
// we don't clear direct UDP paths on disco ping timeout (see
|
||||
// discoPingTimeout).
|
||||
if betterAddr(thisPong, de.bestAddr) {
|
||||
bestUntrusted := now.After(de.trustBestAddrUntil)
|
||||
if betterAddr(thisPong, de.bestAddr) || bestUntrusted {
|
||||
de.c.logf("magicsock: disco: node %v %v now using %v mtu=%v tx=%x", de.publicKey.ShortString(), de.discoShort(), sp.to, thisPong.wireMTU, m.TxID[:6])
|
||||
de.debugUpdates.Add(EndpointChange{
|
||||
When: time.Now(),
|
||||
@@ -2004,7 +2233,7 @@ func (de *endpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
ps.Relay = de.c.derpRegionCodeOfIDLocked(int(de.derpAddr.Port()))
|
||||
ps.Relay = de.c.derpRegionCodeOfIDLocked(tailcfg.DERPRegionID(de.derpAddr.Port()))
|
||||
|
||||
if de.lastSendExt.IsZero() {
|
||||
return
|
||||
@@ -2055,9 +2284,17 @@ func (de *endpoint) stopAndReset() {
|
||||
// DERP-only endpoint. It does not stop the endpoint's heartbeat
|
||||
// timer, if one is running.
|
||||
func (de *endpoint) resetLocked() {
|
||||
de.clearBestAddrLocked()
|
||||
de.invalidateDiscoPathLocked()
|
||||
}
|
||||
|
||||
// invalidateDiscoPathLocked discards in-flight disco/relay state that was
|
||||
// established against the peer's current disco identity, so that path
|
||||
// discovery re-runs from scratch.
|
||||
func (de *endpoint) invalidateDiscoPathLocked() {
|
||||
de.lastSendExt = 0
|
||||
de.lastFullPing = 0
|
||||
de.clearBestAddrLocked()
|
||||
de.lastUDPRelayPathDiscovery = 0
|
||||
for _, es := range de.endpointState {
|
||||
es.lastPing = 0
|
||||
}
|
||||
@@ -2067,7 +2304,9 @@ func (de *endpoint) resetLocked() {
|
||||
}
|
||||
}
|
||||
de.probeUDPLifetime.resetCycleEndpointLocked()
|
||||
de.c.relayManager.stopWork(de)
|
||||
if buildfeatures.HasNATTraversal && de.c != nil {
|
||||
de.c.relayManager.stopWork(de)
|
||||
}
|
||||
}
|
||||
|
||||
func (de *endpoint) numStopAndReset() int64 {
|
||||
@@ -2082,7 +2321,7 @@ func (de *endpoint) setDERPHome(regionID uint16) {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
de.derpAddr = netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(regionID))
|
||||
if de.c.hasPeerRelayServers.Load() {
|
||||
if buildfeatures.HasNATTraversal && de.c.relayManager.hasPeerRelayServers.Load() {
|
||||
de.c.relayManager.handleDERPHomeChange(de.publicKey, regionID)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !wasm && !plan9
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build wasm || plan9
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
+853
-506
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux || ts_omit_listenrawdisco
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !ts_omit_listenrawdisco
|
||||
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
@@ -8,6 +8,8 @@ package magicsock
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/net/neterror"
|
||||
)
|
||||
|
||||
// shouldRebind returns if the error is one that is known to be healed by a
|
||||
@@ -17,7 +19,7 @@ func shouldRebind(err error) (ok bool, reason string) {
|
||||
// EPIPE/ENOTCONN are common errors when a send fails due to a closed
|
||||
// socket. There is some platform and version inconsistency in which
|
||||
// error is returned, but the meaning is the same.
|
||||
case errors.Is(err, syscall.EPIPE), errors.Is(err, syscall.ENOTCONN):
|
||||
case neterror.IsClosedPipeError(err):
|
||||
return true, "broken-pipe"
|
||||
|
||||
// EPERM is typically caused by EDR software, and has been observed to be
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build plan9
|
||||
|
||||
+52
-8
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/set"
|
||||
@@ -48,6 +50,7 @@ type peerMap struct {
|
||||
// byEpAddr. That issue is being tracked in http://go/corp/29422.
|
||||
relayEpAddrByNodeKey map[key.NodePublic]epAddr
|
||||
|
||||
nodesMu sync.RWMutex // protects nodesOfDisco
|
||||
// nodesOfDisco contains the set of nodes that are using a
|
||||
// DiscoKey. Usually those sets will be just one node.
|
||||
nodesOfDisco map[key.DiscoPublic]set.Set[key.NodePublic]
|
||||
@@ -74,6 +77,9 @@ func (m *peerMap) nodeCount() int {
|
||||
// knownPeerDiscoKey reports whether there exists any peer with the disco key
|
||||
// dk.
|
||||
func (m *peerMap) knownPeerDiscoKey(dk key.DiscoPublic) bool {
|
||||
m.nodesMu.RLock()
|
||||
defer m.nodesMu.RUnlock()
|
||||
|
||||
_, ok := m.nodesOfDisco[dk]
|
||||
return ok
|
||||
}
|
||||
@@ -117,8 +123,11 @@ func (m *peerMap) forEachEndpoint(f func(ep *endpoint)) {
|
||||
|
||||
// forEachEndpointWithDiscoKey invokes f on every endpoint in m that has the
|
||||
// provided DiscoKey until f returns false or there are no endpoints left to
|
||||
// iterate.
|
||||
// iterate. f must call back into peerMap and mutate [nodesOfDisco].
|
||||
func (m *peerMap) forEachEndpointWithDiscoKey(dk key.DiscoPublic, f func(*endpoint) (keepGoing bool)) {
|
||||
m.nodesMu.RLock()
|
||||
defer m.nodesMu.RUnlock()
|
||||
|
||||
for nk := range m.nodesOfDisco[dk] {
|
||||
pi, ok := m.byNodeKey[nk]
|
||||
if !ok {
|
||||
@@ -138,7 +147,9 @@ func (m *peerMap) forEachEndpointWithDiscoKey(dk key.DiscoPublic, f func(*endpoi
|
||||
// upsertEndpoint stores endpoint in the peerInfo for
|
||||
// ep.publicKey, and updates indexes. m must already have a
|
||||
// tailcfg.Node for ep.publicKey.
|
||||
func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
|
||||
func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic,
|
||||
fromTSMP bool,
|
||||
) {
|
||||
if ep.nodeID == 0 {
|
||||
panic("internal error: upsertEndpoint called with zero NodeID")
|
||||
}
|
||||
@@ -149,9 +160,14 @@ func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
|
||||
}
|
||||
m.byNodeID[ep.nodeID] = pi
|
||||
|
||||
// Load key and make the comparison based on the source of the new key.
|
||||
epDisco := ep.disco.Load()
|
||||
if epDisco == nil || oldDiscoKey != epDisco.key {
|
||||
delete(m.nodesOfDisco[oldDiscoKey], ep.publicKey)
|
||||
epKey := epDisco.keyFromControl()
|
||||
if fromTSMP {
|
||||
epKey = epDisco.keyFromTSMP()
|
||||
}
|
||||
if epDisco == nil || oldDiscoKey != epKey {
|
||||
m.cleanFromNodesOfDisco(oldDiscoKey, ep.publicKey)
|
||||
}
|
||||
if ep.isWireguardOnly {
|
||||
// If the peer is a WireGuard only peer, add all of its endpoints.
|
||||
@@ -166,12 +182,22 @@ func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
|
||||
}
|
||||
return
|
||||
}
|
||||
discoSet := m.nodesOfDisco[epDisco.key]
|
||||
|
||||
// There is no need to insert a new node key under a zero disco key as it
|
||||
// wastes space and will risk stale entries just sitting there in a long list
|
||||
// under the zero key.
|
||||
if epKey.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
m.nodesMu.Lock()
|
||||
discoSet := m.nodesOfDisco[epKey]
|
||||
if discoSet == nil {
|
||||
discoSet = set.Set[key.NodePublic]{}
|
||||
m.nodesOfDisco[epDisco.key] = discoSet
|
||||
m.nodesOfDisco[epKey] = discoSet
|
||||
}
|
||||
discoSet.Add(ep.publicKey)
|
||||
m.nodesMu.Unlock()
|
||||
}
|
||||
|
||||
// setNodeKeyForEpAddr makes future peer lookups by addr return the
|
||||
@@ -214,8 +240,16 @@ func (m *peerMap) deleteEndpoint(ep *endpoint) {
|
||||
|
||||
pi := m.byNodeKey[ep.publicKey]
|
||||
if epDisco != nil {
|
||||
delete(m.nodesOfDisco[epDisco.key], ep.publicKey)
|
||||
for _, discoKey := range []key.DiscoPublic{
|
||||
epDisco.keyFromControl(), epDisco.keyFromTSMP(),
|
||||
} {
|
||||
if discoKey.IsZero() {
|
||||
continue
|
||||
}
|
||||
m.cleanFromNodesOfDisco(discoKey, ep.publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
delete(m.byNodeKey, ep.publicKey)
|
||||
if was, ok := m.byNodeID[ep.nodeID]; ok && was.ep == ep {
|
||||
delete(m.byNodeID, ep.nodeID)
|
||||
@@ -230,3 +264,13 @@ func (m *peerMap) deleteEndpoint(ep *endpoint) {
|
||||
}
|
||||
delete(m.relayEpAddrByNodeKey, ep.publicKey)
|
||||
}
|
||||
|
||||
func (m *peerMap) cleanFromNodesOfDisco(disco key.DiscoPublic, node key.NodePublic) {
|
||||
m.nodesMu.Lock()
|
||||
defer m.nodesMu.Unlock()
|
||||
s := m.nodesOfDisco[disco]
|
||||
delete(s, node)
|
||||
if len(s) == 0 {
|
||||
delete(m.nodesOfDisco, disco)
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (darwin && !ios) || (linux && !android)
|
||||
@@ -84,6 +84,9 @@ func (c *Conn) PeerMTUEnabled() bool {
|
||||
// If the PMTUD settings changed, it resets the endpoint state so that it will
|
||||
// re-probe path MTUs to this peer.
|
||||
func (c *Conn) UpdatePMTUD() {
|
||||
if !hasUDPTransport {
|
||||
return
|
||||
}
|
||||
if debugPMTUD() {
|
||||
df4, err4 := c.getDontFragment("udp4")
|
||||
df6, err6 := c.getDontFragment("udp6")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build darwin && !ios
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !android
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (!linux && !darwin) || android || ios
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (darwin && !ios) || (linux && !android)
|
||||
|
||||
+15
-12
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/ipv6"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/net/batching"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
@@ -41,11 +41,13 @@ type RebindingUDPConn struct {
|
||||
// nettype.PacketConn to a batchingConn when appropriate. This upgrade is
|
||||
// intentionally pushed closest to where read/write ops occur in order to avoid
|
||||
// disrupting surrounding code that assumes nettype.PacketConn is a
|
||||
// *net.UDPConn.
|
||||
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, batchSize int) {
|
||||
upc := batching.TryUpgradeToConn(p, network, batchSize)
|
||||
c.pconn = upc
|
||||
c.pconnAtomic.Store(&upc)
|
||||
// *net.UDPConn. knobs may be nil.
|
||||
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, knobs *controlknobs.Knobs) {
|
||||
if hasUDPTransport {
|
||||
p = batching.TryUpgradeToConn(p, network, "magicsock_udp_rxq_overflows", knobs)
|
||||
}
|
||||
c.pconn = p
|
||||
c.pconnAtomic.Store(&p)
|
||||
c.port = uint16(c.localAddrLocked().Port)
|
||||
}
|
||||
|
||||
@@ -118,20 +120,21 @@ func (c *RebindingUDPConn) WriteWireGuardBatchTo(buffs [][]byte, addr epAddr, of
|
||||
|
||||
// ReadBatch is an alias for [batching.Conn.ReadBatch] with fallback to single
|
||||
// packet operations if c.pconn is not a [batching.Conn].
|
||||
func (c *RebindingUDPConn) ReadBatch(msgs []ipv6.Message, flags int) (int, error) {
|
||||
func (c *RebindingUDPConn) ReadBatch(slab []byte, batchingPackets []batching.ReceivedPacket) (int, error) {
|
||||
for {
|
||||
pconn := *c.pconnAtomic.Load()
|
||||
b, ok := pconn.(batching.Conn)
|
||||
if !ok {
|
||||
n, ap, err := c.readFromWithInitPconn(pconn, msgs[0].Buffers[0])
|
||||
n, ap, err := c.readFromWithInitPconn(pconn, slab)
|
||||
if err == nil {
|
||||
msgs[0].N = n
|
||||
msgs[0].Addr = net.UDPAddrFromAddrPort(netaddr.Unmap(ap))
|
||||
batchingPackets[0].Offset = 0
|
||||
batchingPackets[0].Size = n
|
||||
batchingPackets[0].Source = netaddr.Unmap(ap)
|
||||
return 1, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
n, err := b.ReadBatch(msgs, flags)
|
||||
n, err := b.ReadBatch(slab, batchingPackets)
|
||||
if err != nil && pconn != c.currentConn() {
|
||||
continue
|
||||
}
|
||||
|
||||
+87
-8
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package magicsock
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/disco"
|
||||
@@ -34,6 +35,14 @@ import (
|
||||
type relayManager struct {
|
||||
initOnce sync.Once
|
||||
|
||||
// hasPeerRelayServers is whether relayManager is configured with at
|
||||
// least one peer relay server via [relayManager.handleRelayServersSet]
|
||||
// (or per-peer variants). Exposed as an atomic so [endpoint] hot paths
|
||||
// can short-circuit when there are no relay servers without taking any
|
||||
// lock or entering the run loop. Written only from runLoop() via
|
||||
// [relayManager.publishHasServersRunLoop].
|
||||
hasPeerRelayServers atomic.Bool
|
||||
|
||||
// ===================================================================
|
||||
// The following fields are owned by a single goroutine, runLoop().
|
||||
serversByNodeKey map[key.NodePublic]candidatePeerRelay
|
||||
@@ -56,6 +65,8 @@ type relayManager struct {
|
||||
newServerEndpointCh chan newRelayServerEndpointEvent
|
||||
rxDiscoMsgCh chan relayDiscoMsgEvent
|
||||
serversCh chan set.Set[candidatePeerRelay]
|
||||
serverUpsertCh chan candidatePeerRelay
|
||||
serverRemoveCh chan key.NodePublic
|
||||
getServersCh chan chan set.Set[candidatePeerRelay]
|
||||
derpHomeChangeCh chan derpHomeChangeEvent
|
||||
|
||||
@@ -228,6 +239,16 @@ func (r *relayManager) runLoop() {
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
return
|
||||
}
|
||||
case upsert := <-r.serverUpsertCh:
|
||||
r.handleServerUpsertRunLoop(upsert)
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
return
|
||||
}
|
||||
case nk := <-r.serverRemoveCh:
|
||||
r.handleServerRemoveRunLoop(nk)
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
return
|
||||
}
|
||||
case getServersCh := <-r.getServersCh:
|
||||
r.handleGetServersRunLoop(getServersCh)
|
||||
if !r.hasActiveWorkRunLoop() {
|
||||
@@ -265,6 +286,34 @@ func (r *relayManager) handleServersUpdateRunLoop(update set.Set[candidatePeerRe
|
||||
for _, v := range update.Slice() {
|
||||
r.serversByNodeKey[v.nodeKey] = v
|
||||
}
|
||||
r.publishHasServersRunLoop()
|
||||
}
|
||||
|
||||
// handleServerUpsertRunLoop inserts or updates cp in serversByNodeKey. It is
|
||||
// the per-peer analog of [relayManager.handleServersUpdateRunLoop] used by
|
||||
// [Conn.UpsertPeer].
|
||||
func (r *relayManager) handleServerUpsertRunLoop(cp candidatePeerRelay) {
|
||||
r.serversByNodeKey[cp.nodeKey] = cp
|
||||
r.publishHasServersRunLoop()
|
||||
}
|
||||
|
||||
// handleServerRemoveRunLoop deletes nk from serversByNodeKey. It is a no-op
|
||||
// if nk isn't currently a known server. It is the per-peer analog of
|
||||
// [relayManager.handleServersUpdateRunLoop] used by [Conn.RemovePeer] and by
|
||||
// [Conn.UpsertPeer] when a peer is upserted with fields that make it no
|
||||
// longer a relay candidate.
|
||||
func (r *relayManager) handleServerRemoveRunLoop(nk key.NodePublic) {
|
||||
if _, ok := r.serversByNodeKey[nk]; !ok {
|
||||
return
|
||||
}
|
||||
delete(r.serversByNodeKey, nk)
|
||||
r.publishHasServersRunLoop()
|
||||
}
|
||||
|
||||
// publishHasServersRunLoop updates [relayManager.hasPeerRelayServers] to
|
||||
// reflect whether any relay servers are currently known.
|
||||
func (r *relayManager) publishHasServersRunLoop() {
|
||||
r.hasPeerRelayServers.Store(len(r.serversByNodeKey) > 0)
|
||||
}
|
||||
|
||||
type relayDiscoMsgEvent struct {
|
||||
@@ -330,6 +379,8 @@ func (r *relayManager) init() {
|
||||
r.newServerEndpointCh = make(chan newRelayServerEndpointEvent)
|
||||
r.rxDiscoMsgCh = make(chan relayDiscoMsgEvent)
|
||||
r.serversCh = make(chan set.Set[candidatePeerRelay])
|
||||
r.serverUpsertCh = make(chan candidatePeerRelay)
|
||||
r.serverRemoveCh = make(chan key.NodePublic)
|
||||
r.getServersCh = make(chan chan set.Set[candidatePeerRelay])
|
||||
r.derpHomeChangeCh = make(chan derpHomeChangeEvent)
|
||||
r.runLoopStoppedCh = make(chan struct{}, 1)
|
||||
@@ -436,6 +487,21 @@ func (r *relayManager) handleRelayServersSet(servers set.Set[candidatePeerRelay]
|
||||
relayManagerInputEvent(r, nil, &r.serversCh, servers)
|
||||
}
|
||||
|
||||
// handleRelayServerUpsert is the O(1) per-peer variant of
|
||||
// [relayManager.handleRelayServersSet]: it inserts or updates a single
|
||||
// relay server entry.
|
||||
func (r *relayManager) handleRelayServerUpsert(cp candidatePeerRelay) {
|
||||
relayManagerInputEvent(r, nil, &r.serverUpsertCh, cp)
|
||||
}
|
||||
|
||||
// handleRelayServerRemove is the O(1) per-peer variant of
|
||||
// [relayManager.handleRelayServersSet]: it removes a single relay server
|
||||
// entry by node key. It is a no-op if nk is not currently a known relay
|
||||
// server.
|
||||
func (r *relayManager) handleRelayServerRemove(nk key.NodePublic) {
|
||||
relayManagerInputEvent(r, nil, &r.serverRemoveCh, nk)
|
||||
}
|
||||
|
||||
// relayManagerInputEvent initializes [relayManager] if necessary, starts
|
||||
// relayManager.runLoop() if it is not running, and writes 'event' on 'eventCh'.
|
||||
//
|
||||
@@ -679,6 +745,17 @@ func (r *relayManager) handleHandshakeWorkDoneRunLoop(done relayEndpointHandshak
|
||||
}
|
||||
|
||||
func (r *relayManager) handleNewServerEndpointRunLoop(newServerEndpoint newRelayServerEndpointEvent) {
|
||||
if newServerEndpoint.se.ServerDisco.IsZero() {
|
||||
// A relay server always has a nonzero disco key, so this endpoint is
|
||||
// malformed or malicious. It may come from a [disco.CallMeMaybeVia]
|
||||
// with a zeroed ServerDisco, including one carrying an unknown
|
||||
// message version, which parses to a zero-valued message. Dropping it
|
||||
// here prevents a panic in [relayManager.ensureDiscoInfoFor], whose
|
||||
// DiscoPrivate.Shared call rejects zero keys.
|
||||
newServerEndpoint.wlb.ep.c.logf("magicsock: relayManager: ignoring relay server endpoint with zero ServerDisco")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate work by server disco + VNI.
|
||||
sdv := serverDiscoVNI{newServerEndpoint.se.ServerDisco, newServerEndpoint.se.VNI}
|
||||
existingWork, ok := r.handshakeWorkByServerDiscoVNI[sdv]
|
||||
@@ -724,8 +801,10 @@ func (r *relayManager) handleNewServerEndpointRunLoop(newServerEndpoint newRelay
|
||||
go r.sendCallMeMaybeVia(newServerEndpoint.wlb.ep, newServerEndpoint.se)
|
||||
}
|
||||
|
||||
lastBestMatchingServer := newServerEndpoint.se.ServerDisco.Compare(newServerEndpoint.wlb.lastBest.relayServerDisco) == 0
|
||||
if lastBestMatchingServer && newServerEndpoint.wlb.lastBestIsTrusted {
|
||||
lastBestMatchingServerVNI := newServerEndpoint.wlb.lastBest.vni.IsSet() &&
|
||||
newServerEndpoint.se.ServerDisco.Compare(newServerEndpoint.wlb.lastBest.relayServerDisco) == 0 &&
|
||||
newServerEndpoint.se.VNI == newServerEndpoint.wlb.lastBest.vni.Get()
|
||||
if lastBestMatchingServerVNI && newServerEndpoint.wlb.lastBestIsTrusted {
|
||||
// This relay server endpoint is the same as [endpoint]'s bestAddr at
|
||||
// the time UDP relay path discovery was started, and it was also a
|
||||
// trusted path (see endpoint.trustBestAddrUntil), so return early.
|
||||
@@ -801,7 +880,7 @@ func (r *relayManager) sendCallMeMaybeVia(ep *endpoint, se udprelay.ServerEndpoi
|
||||
AddrPorts: se.AddrPorts,
|
||||
},
|
||||
}
|
||||
ep.c.sendDiscoMessage(epAddr{ap: derpAddr}, ep.publicKey, epDisco.key, callMeMaybeVia, discoVerboseLog)
|
||||
ep.c.sendDiscoMessage(epAddr{ap: derpAddr}, ep.publicKey, epDisco.key(), callMeMaybeVia, discoVerboseLog)
|
||||
}
|
||||
|
||||
func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
@@ -824,7 +903,7 @@ func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
common := disco.BindUDPRelayEndpointCommon{
|
||||
VNI: work.se.VNI,
|
||||
Generation: work.handshakeGen,
|
||||
RemoteKey: epDisco.key,
|
||||
RemoteKey: epDisco.key(),
|
||||
}
|
||||
|
||||
work.dlogf("[v1] magicsock: relayManager: starting handshake addrPorts=%v",
|
||||
@@ -878,7 +957,7 @@ func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
answer.Challenge = *withAnswer
|
||||
ep.c.sendDiscoMessage(epAddr{ap: to, vni: vni}, key.NodePublic{}, work.se.ServerDisco, answer, discoVerboseLog)
|
||||
}
|
||||
ep.c.sendDiscoMessage(epAddr{ap: to, vni: vni}, key.NodePublic{}, epDisco.key, ping, discoVerboseLog)
|
||||
ep.c.sendDiscoMessage(epAddr{ap: to, vni: vni}, key.NodePublic{}, epDisco.key(), ping, discoVerboseLog)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -886,7 +965,7 @@ func (r *relayManager) handshakeServerEndpoint(work *relayHandshakeWork) {
|
||||
if common.VNI != work.se.VNI {
|
||||
return errors.New("mismatching VNI")
|
||||
}
|
||||
if common.RemoteKey.Compare(epDisco.key) != 0 {
|
||||
if common.RemoteKey.Compare(epDisco.key()) != 0 {
|
||||
return errors.New("mismatching RemoteKey")
|
||||
}
|
||||
return nil
|
||||
@@ -1031,7 +1110,7 @@ func (r *relayManager) allocateAllServersRunLoop(wlb endpointWithLastBest) {
|
||||
if remoteDisco == nil {
|
||||
return
|
||||
}
|
||||
discoKeys := key.NewSortedPairOfDiscoPublic(wlb.ep.c.discoAtomic.Public(), remoteDisco.key)
|
||||
discoKeys := key.NewSortedPairOfDiscoPublic(wlb.ep.c.discoAtomic.Public(), remoteDisco.key())
|
||||
for _, v := range r.serversByNodeKey {
|
||||
byDiscoKeys, ok := r.allocWorkByDiscoKeysByServerNodeKey[v.nodeKey]
|
||||
if !ok {
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package wgengine
|
||||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
)
|
||||
|
||||
// iOS has a very restrictive memory limit on network extensions.
|
||||
// Reduce the maximum amount of memory that wireguard-go can allocate
|
||||
// to avoid getting killed.
|
||||
|
||||
func init() {
|
||||
device.QueueStagedSize = 64
|
||||
device.QueueOutboundSize = 64
|
||||
device.QueueInboundSize = 64
|
||||
device.QueueHandshakeSize = 64
|
||||
device.PreallocatedBuffersPerPool = 64
|
||||
}
|
||||
+40
-57
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_netlog && !ts_omit_logtail
|
||||
@@ -24,12 +24,12 @@ import (
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netlogfunc"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -51,6 +51,19 @@ type noopDevice struct{}
|
||||
|
||||
func (noopDevice) SetConnectionCounter(netlogfunc.ConnectionCounter) {}
|
||||
|
||||
// NodeSource provides node lookups for the network logger.
|
||||
// Methods may be called concurrently.
|
||||
type NodeSource interface {
|
||||
// SelfNode returns the local node and its owning user profile.
|
||||
// Both views may be invalid if no self node is known yet.
|
||||
SelfNode() (node tailcfg.NodeView, user tailcfg.UserProfileView)
|
||||
|
||||
// NodeByAddr returns the node assigned the given address along with
|
||||
// its owning user profile.
|
||||
// ok is false if no node is known to own addr.
|
||||
NodeByAddr(addr netip.Addr) (node tailcfg.NodeView, user tailcfg.UserProfileView, ok bool)
|
||||
}
|
||||
|
||||
// Logger logs statistics about every connection.
|
||||
// At present, it only logs connections within a tailscale network.
|
||||
// By default, exit node traffic is not logged for privacy reasons
|
||||
@@ -69,10 +82,8 @@ type Logger struct {
|
||||
recordsChan chan record // set to nil when shutdown
|
||||
flushTimer *time.Timer // fires when record should flush to recordsChan
|
||||
|
||||
// Information about Tailscale nodes.
|
||||
// These are read-only once updated by ReconfigNetworkMap.
|
||||
selfNode nodeUser
|
||||
allNodes map[netip.Addr]nodeUser // includes selfNode; nodeUser values are always valid
|
||||
// source provides node lookups. Set by Startup, cleared by shutdownLocked.
|
||||
source NodeSource
|
||||
|
||||
// Information about routes.
|
||||
// These are read-only once updated by ReconfigRoutes.
|
||||
@@ -115,14 +126,20 @@ var testClient *http.Client
|
||||
// The sock is used to populated the PhysicalTraffic field in [netlogtype.Message].
|
||||
//
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func (nl *Logger) Startup(logf logger.Logf, nm *netmap.NetworkMap, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, health *health.Tracker, bus *eventbus.Bus, logExitFlowEnabledEnabled bool) error {
|
||||
//
|
||||
// source provides on-demand node lookups for connections seen by the logger.
|
||||
// It must be non-nil.
|
||||
func (nl *Logger) Startup(logf logger.Logf, source NodeSource, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, health *health.Tracker, bus *eventbus.Bus, logExitFlowEnabledEnabled bool) error {
|
||||
nl.mu.Lock()
|
||||
defer nl.mu.Unlock()
|
||||
|
||||
if nl.shutdownLocked != nil {
|
||||
return fmt.Errorf("network logger already running")
|
||||
}
|
||||
nl.selfNode, nl.allNodes = makeNodeMaps(nm)
|
||||
if source == nil {
|
||||
return fmt.Errorf("network logger requires a non-nil NodeSource")
|
||||
}
|
||||
nl.source = source
|
||||
|
||||
// Startup a log stream to Tailscale's logging service.
|
||||
if logf == nil {
|
||||
@@ -194,8 +211,7 @@ func (nl *Logger) Startup(logf logger.Logf, nm *netmap.NetworkMap, nodeLogID, do
|
||||
|
||||
// Purge state.
|
||||
nl.shutdownLocked = nil
|
||||
nl.selfNode = nodeUser{}
|
||||
nl.allNodes = nil
|
||||
nl.source = nil
|
||||
nl.routeAddrs = nil
|
||||
nl.routePrefixes = nil
|
||||
|
||||
@@ -249,15 +265,15 @@ func (nl *Logger) addNewVirtConnLocked(c netlogtype.Connection) connType {
|
||||
var srcNodeLen, dstNodeLen int
|
||||
srcNode, srcSeen := nl.record.seenNodes[c.Src.Addr()]
|
||||
if !srcSeen {
|
||||
srcNode = nl.allNodes[c.Src.Addr()]
|
||||
if srcNode.Valid() {
|
||||
if node, user, ok := nl.source.NodeByAddr(c.Src.Addr()); ok {
|
||||
srcNode = nodeUser{node, user}
|
||||
srcNodeLen = srcNode.jsonLen()
|
||||
}
|
||||
}
|
||||
dstNode, dstSeen := nl.record.seenNodes[c.Dst.Addr()]
|
||||
if !dstSeen {
|
||||
dstNode = nl.allNodes[c.Dst.Addr()]
|
||||
if dstNode.Valid() {
|
||||
if node, user, ok := nl.source.NodeByAddr(c.Dst.Addr()); ok {
|
||||
dstNode = nodeUser{node, user}
|
||||
dstNodeLen = dstNode.jsonLen()
|
||||
}
|
||||
}
|
||||
@@ -279,12 +295,10 @@ func (nl *Logger) addNewVirtConnLocked(c netlogtype.Connection) connType {
|
||||
nl.recordLen += netlogtype.MaxConnectionCountsJSONSize + srcNodeLen + dstNodeLen
|
||||
|
||||
// Classify the traffic type.
|
||||
var srcIsSelfNode bool
|
||||
if nl.selfNode.Valid() {
|
||||
srcIsSelfNode = nl.selfNode.Addresses().ContainsFunc(func(p netip.Prefix) bool {
|
||||
return c.Src.Addr() == p.Addr() && p.IsSingleIP()
|
||||
})
|
||||
}
|
||||
// srcNode == self iff NodeByAddr resolved the source to the same node as
|
||||
// the current record's self.
|
||||
srcIsSelfNode := srcNode.Valid() && nl.record.selfNode.Valid() &&
|
||||
srcNode.ID() == nl.record.selfNode.ID()
|
||||
switch {
|
||||
case srcIsSelfNode && dstNode.Valid():
|
||||
return virtualTraffic
|
||||
@@ -335,8 +349,8 @@ func (nl *Logger) addNewPhysConnLocked(c netlogtype.Connection) {
|
||||
var srcNodeLen int
|
||||
srcNode, srcSeen := nl.record.seenNodes[c.Src.Addr()]
|
||||
if !srcSeen {
|
||||
srcNode = nl.allNodes[c.Src.Addr()]
|
||||
if srcNode.Valid() {
|
||||
if node, user, ok := nl.source.NodeByAddr(c.Src.Addr()); ok {
|
||||
srcNode = nodeUser{node, user}
|
||||
srcNodeLen = srcNode.jsonLen()
|
||||
}
|
||||
}
|
||||
@@ -361,14 +375,16 @@ func (nl *Logger) initRecordLocked() {
|
||||
if nl.recordLen != 0 {
|
||||
return
|
||||
}
|
||||
node, user := nl.source.SelfNode()
|
||||
self := nodeUser{node, user}
|
||||
nl.record = record{
|
||||
selfNode: nl.selfNode,
|
||||
selfNode: self,
|
||||
start: time.Now().UTC(),
|
||||
seenNodes: make(map[netip.Addr]nodeUser),
|
||||
virtConns: make(map[netlogtype.Connection]countsType),
|
||||
physConns: make(map[netlogtype.Connection]netlogtype.Counts),
|
||||
}
|
||||
nl.recordLen = netlogtype.MinMessageJSONSize + nl.selfNode.jsonLen()
|
||||
nl.recordLen = netlogtype.MinMessageJSONSize + self.jsonLen()
|
||||
|
||||
// Start a time to auto-flush the record.
|
||||
// Avoid tickers since continually waking up a goroutine
|
||||
@@ -406,39 +422,6 @@ func (nl *Logger) flushRecordLocked() {
|
||||
nl.recordLen = 0
|
||||
}
|
||||
|
||||
func makeNodeMaps(nm *netmap.NetworkMap) (selfNode nodeUser, allNodes map[netip.Addr]nodeUser) {
|
||||
if nm == nil {
|
||||
return
|
||||
}
|
||||
allNodes = make(map[netip.Addr]nodeUser)
|
||||
if nm.SelfNode.Valid() {
|
||||
selfNode = nodeUser{nm.SelfNode, nm.UserProfiles[nm.SelfNode.User()]}
|
||||
for _, addr := range nm.SelfNode.Addresses().All() {
|
||||
if addr.IsSingleIP() {
|
||||
allNodes[addr.Addr()] = selfNode
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, peer := range nm.Peers {
|
||||
if peer.Valid() {
|
||||
for _, addr := range peer.Addresses().All() {
|
||||
if addr.IsSingleIP() {
|
||||
allNodes[addr.Addr()] = nodeUser{peer, nm.UserProfiles[peer.User()]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selfNode, allNodes
|
||||
}
|
||||
|
||||
// ReconfigNetworkMap configures the network logger with an updated netmap.
|
||||
func (nl *Logger) ReconfigNetworkMap(nm *netmap.NetworkMap) {
|
||||
selfNode, allNodes := makeNodeMaps(nm) // avoid holding lock while making maps
|
||||
nl.mu.Lock()
|
||||
nl.selfNode, nl.allNodes = selfNode, allNodes
|
||||
nl.mu.Unlock()
|
||||
}
|
||||
|
||||
func makeRouteMaps(cfg *router.Config) (addrs set.Set[netip.Addr], prefixes []netip.Prefix) {
|
||||
addrs = make(set.Set[netip.Addr])
|
||||
insertPrefixes := func(rs []netip.Prefix) {
|
||||
|
||||
+9
-6
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_netlog || ts_omit_logtail
|
||||
@@ -7,8 +7,11 @@ package netlog
|
||||
|
||||
type Logger struct{}
|
||||
|
||||
func (*Logger) Startup(...any) error { return nil }
|
||||
func (*Logger) Running() bool { return false }
|
||||
func (*Logger) Shutdown(any) error { return nil }
|
||||
func (*Logger) ReconfigNetworkMap(any) {}
|
||||
func (*Logger) ReconfigRoutes(any) {}
|
||||
// NodeSource is a stub kept so the omit build does not break consumers that
|
||||
// reference the type. It has no methods.
|
||||
type NodeSource any
|
||||
|
||||
func (*Logger) Startup(...any) error { return nil }
|
||||
func (*Logger) Running() bool { return false }
|
||||
func (*Logger) Shutdown(any) error { return nil }
|
||||
func (*Logger) ReconfigRoutes(any) {}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_netlog && !ts_omit_logtail
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package wgengine
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// NetLogSource provides the network flow logging feature what it needs
|
||||
// from the rest of the system (in practice, ipnlocal.LocalBackend):
|
||||
// on-demand node lookups and the current audit logging identity.
|
||||
type NetLogSource interface {
|
||||
// SelfNode returns the current self node and its owning user profile.
|
||||
// The views are invalid if there is no current node.
|
||||
SelfNode() (tailcfg.NodeView, tailcfg.UserProfileView)
|
||||
|
||||
// NodeByAddr returns the node assigned the given address along with
|
||||
// its owning user profile.
|
||||
// ok is false if no node is known to own addr.
|
||||
NodeByAddr(addr netip.Addr) (node tailcfg.NodeView, user tailcfg.UserProfileView, ok bool)
|
||||
|
||||
// NetLogIDs returns the audit logging IDs from the current netmap,
|
||||
// along with whether exit node flows should be logged.
|
||||
// ok is false if network flow logging should not run.
|
||||
NetLogIDs() (nodeID, domainID logid.PrivateID, logExitFlows bool, ok bool)
|
||||
}
|
||||
|
||||
// NetLogger is the engine's handle on the network flow logger
|
||||
// lifecycle, implemented by the feature/netlog package.
|
||||
//
|
||||
// Reconfig and ReconfigDone are only called from [Engine.Reconfig],
|
||||
// serialized by the engine's internal lock. Shutdown may be called
|
||||
// concurrently with them from [Engine.Close].
|
||||
type NetLogger interface {
|
||||
// Reconfig is called near the top of every [Engine.Reconfig],
|
||||
// before its ErrNoChanges early return (so identity-only changes
|
||||
// still take effect) and before the router is configured (so a
|
||||
// starting logger captures initial packets). It reports whether
|
||||
// network logging state changed, which suppresses the engine's
|
||||
// ErrNoChanges early return.
|
||||
Reconfig(routerCfg *router.Config, routerChanged bool) (changed bool)
|
||||
|
||||
// ReconfigDone is called at the end of [Engine.Reconfig], after
|
||||
// the router is configured, so that a logger stopping as a result
|
||||
// of the preceding Reconfig call captures final packets. It may
|
||||
// block to flush pending messages.
|
||||
ReconfigDone()
|
||||
|
||||
// Shutdown stops the logger. It may block to flush pending
|
||||
// messages, bounded by an internal upload timeout.
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// NetLoggerDeps are the engine-owned dependencies passed to
|
||||
// [HookNewNetLogger] to construct a [NetLogger].
|
||||
type NetLoggerDeps struct {
|
||||
Logf logger.Logf
|
||||
Tun *tstun.Wrapper
|
||||
Sock *magicsock.Conn
|
||||
NetMon *netmon.Monitor
|
||||
Health *health.Tracker
|
||||
Bus *eventbus.Bus
|
||||
|
||||
// Source returns the [NetLogSource] installed via
|
||||
// [Engine.SetNetLogSource], or nil if none has been installed yet.
|
||||
// It is a func because the source is installed after the engine
|
||||
// (and thus the NetLogger) is constructed.
|
||||
Source func() NetLogSource
|
||||
}
|
||||
|
||||
// HookNewNetLogger is set by the feature/netlog package to construct
|
||||
// each engine's [NetLogger]. If unset, network flow logging is not
|
||||
// linked into the binary and the engine skips all NetLogger calls.
|
||||
var HookNewNetLogger feature.Hook[func(NetLoggerDeps) NetLogger]
|
||||
+11
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_netstack
|
||||
@@ -25,11 +25,14 @@ import (
|
||||
// !stack.PacketBuffer.RXChecksumValidated, i.e. it satisfies
|
||||
// stack.CapabilityRXChecksumOffload. Other protocols with checksum fields,
|
||||
// e.g. ICMP{v6}, are still validated by gVisor regardless of rx checksum
|
||||
// offloading capabilities.
|
||||
// offloading capabilities. IPv4 fragments cannot have their L4 checksums
|
||||
// validated before reassembly, so only their IPv4 header checksum is validated
|
||||
// here.
|
||||
func RXChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
|
||||
var (
|
||||
pn tcpip.NetworkProtocolNumber
|
||||
csumStart int
|
||||
fragment bool
|
||||
)
|
||||
buf := p.Buffer()
|
||||
|
||||
@@ -45,6 +48,11 @@ func RXChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
|
||||
if ^tun.Checksum(buf[:csumStart], 0) != 0 {
|
||||
return nil
|
||||
}
|
||||
// Non-first fragments (FragmentOffset != 0) arrive here with
|
||||
// p.IPProto == ipproto.Fragment (set by packet.Parsed.Decode), so
|
||||
// they already skip the L4 checksum check below. We only need to
|
||||
// catch the first fragment, which still has its real IPProto.
|
||||
fragment = header.IPv4(buf).More()
|
||||
pn = header.IPv4ProtocolNumber
|
||||
case 6:
|
||||
if len(buf) < header.IPv6FixedHeaderSize {
|
||||
@@ -80,7 +88,7 @@ func RXChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
if p.IPProto == ipproto.TCP || p.IPProto == ipproto.UDP {
|
||||
if !fragment && (p.IPProto == ipproto.TCP || p.IPProto == ipproto.UDP) {
|
||||
lenForPseudo := len(buf) - csumStart
|
||||
csum := tun.PseudoHeaderChecksum(
|
||||
uint8(p.IPProto),
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !ts_omit_gro
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios || ts_omit_gro
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_netstack
|
||||
|
||||
+125
-28
@@ -1,12 +1,14 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netstack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
@@ -97,31 +99,65 @@ const (
|
||||
tcpGROSupported
|
||||
)
|
||||
|
||||
// linkEndpoint implements stack.LinkEndpoint and stack.GSOEndpoint. Outbound
|
||||
// packets written by gVisor towards Tailscale are stored in a channel.
|
||||
// Inbound is fed to gVisor via injectInbound or gro. This is loosely
|
||||
// modeled after gvisor.dev/pkg/tcpip/link/channel.Endpoint.
|
||||
// outboundQueueRouter is a function that takes an input packet and returns an
|
||||
// associated [outboundQueue] decision.
|
||||
type outboundQueueRouter func(*stack.PacketBuffer) outboundQueue
|
||||
|
||||
// outboundQueue describes outbound [queue]'s attached to [linkEndpoint].
|
||||
type outboundQueue int
|
||||
|
||||
const (
|
||||
// outboundDrop indicates that the packet should be discarded. It does not
|
||||
// correspond to a real queue.
|
||||
outboundDrop outboundQueue = iota
|
||||
// outboundToWireGuard is the outboundQueue for packets that should be
|
||||
// delivered to WireGuard for potential transmission to a remote peer.
|
||||
outboundToWireGuard
|
||||
// outboundToHost is the outboundQueue for packets that should be delivered
|
||||
// to the local host networking stack.
|
||||
outboundToHost
|
||||
// outboundLoopback is the outboundQueue for packets that should be looped
|
||||
// back into gVisor.
|
||||
outboundLoopback
|
||||
// outboundQueueLimit is the exclusive upper bound of outboundQueue values.
|
||||
outboundQueueLimit
|
||||
)
|
||||
|
||||
// linkEndpoint implements [stack.LinkEndpoint] and [stack.GSOEndpoint]. Outbound
|
||||
// packets written by gVisor towards Tailscale are stored in one of several
|
||||
// outbound [queue]'s. Inbound is fed to gVisor via [linkEndpoint.injectInbound]
|
||||
// or [linkEndpoint.gro]. linkEndpoint is loosely modeled after
|
||||
// [gvisor.dev/pkg/tcpip/link/channel.Endpoint].
|
||||
type linkEndpoint struct {
|
||||
SupportedGSOKind stack.SupportedGSO
|
||||
supportedGRO supportedGRO
|
||||
SupportedGSOKind stack.SupportedGSO
|
||||
supportedGRO supportedGRO
|
||||
outboundQueueRouter outboundQueueRouter
|
||||
|
||||
mu sync.RWMutex // mu guards the following fields
|
||||
dispatcher stack.NetworkDispatcher
|
||||
linkAddr tcpip.LinkAddress
|
||||
mtu uint32
|
||||
|
||||
q *queue // outbound
|
||||
outboundQueues [outboundQueueLimit]*queue // outbound
|
||||
}
|
||||
|
||||
func newLinkEndpoint(size int, mtu uint32, linkAddr tcpip.LinkAddress, supportedGRO supportedGRO) *linkEndpoint {
|
||||
// newLinkEndpoint constructs a [*linkEndpoint]. size is applied independently
|
||||
// to each outbound [queue], so total size = num outbound queues * size.
|
||||
func newLinkEndpoint(size int, mtu uint32, linkAddr tcpip.LinkAddress, supportedGRO supportedGRO, outboundQueueRouter outboundQueueRouter) *linkEndpoint {
|
||||
le := &linkEndpoint{
|
||||
supportedGRO: supportedGRO,
|
||||
q: &queue{
|
||||
supportedGRO: supportedGRO,
|
||||
outboundQueueRouter: outboundQueueRouter,
|
||||
mtu: mtu,
|
||||
linkAddr: linkAddr,
|
||||
}
|
||||
for n := range le.outboundQueues {
|
||||
if n == int(outboundDrop) {
|
||||
continue
|
||||
}
|
||||
le.outboundQueues[n] = &queue{
|
||||
c: make(chan *stack.PacketBuffer, size),
|
||||
closedCh: make(chan struct{}),
|
||||
},
|
||||
mtu: mtu,
|
||||
linkAddr: linkAddr,
|
||||
}
|
||||
}
|
||||
return le
|
||||
}
|
||||
@@ -151,36 +187,52 @@ func (ep *linkEndpoint) gro(p *packet.Parsed, g *gro.GRO) *gro.GRO {
|
||||
return g
|
||||
}
|
||||
|
||||
// Close closes l. Further packet injections will return an error, and all
|
||||
// Close closes ep. Further packet injections will return an error, and all
|
||||
// pending packets are discarded. Close may be called concurrently with
|
||||
// WritePackets.
|
||||
func (ep *linkEndpoint) Close() {
|
||||
ep.mu.Lock()
|
||||
ep.dispatcher = nil
|
||||
ep.mu.Unlock()
|
||||
ep.q.Close()
|
||||
for _, q := range ep.outboundQueues {
|
||||
if q != nil {
|
||||
q.Close()
|
||||
}
|
||||
}
|
||||
ep.Drain()
|
||||
}
|
||||
|
||||
// Read does non-blocking read one packet from the outbound packet queue.
|
||||
func (ep *linkEndpoint) Read() *stack.PacketBuffer {
|
||||
return ep.q.Read()
|
||||
// Read reads from the provided [outboundQueue] in non-blocking fashion.
|
||||
func (ep *linkEndpoint) Read(queue outboundQueue) *stack.PacketBuffer {
|
||||
return ep.outboundQueues[queue].Read()
|
||||
}
|
||||
|
||||
// ReadContext does blocking read for one packet from the outbound packet queue.
|
||||
// It can be cancelled by ctx, and in this case, it returns nil.
|
||||
func (ep *linkEndpoint) ReadContext(ctx context.Context) *stack.PacketBuffer {
|
||||
return ep.q.ReadContext(ctx)
|
||||
// ReadContext reads from the provided [outboundQueue] in blocking fashion. It
|
||||
// can be canceled by ctx, in which case it will return nil.
|
||||
func (ep *linkEndpoint) ReadContext(ctx context.Context, queue outboundQueue) *stack.PacketBuffer {
|
||||
return ep.outboundQueues[queue].ReadContext(ctx)
|
||||
}
|
||||
|
||||
// Drain removes all outbound packets from the channel and counts them.
|
||||
// Drain drains all packets from outbound queues and returns the number drained.
|
||||
func (ep *linkEndpoint) Drain() int {
|
||||
return ep.q.Drain()
|
||||
var total int
|
||||
for _, q := range ep.outboundQueues {
|
||||
if q != nil {
|
||||
total += q.Drain()
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// NumQueued returns the number of packets queued for outbound.
|
||||
func (ep *linkEndpoint) NumQueued() int {
|
||||
return ep.q.Num()
|
||||
var total int
|
||||
for _, q := range ep.outboundQueues {
|
||||
if q != nil {
|
||||
total += q.Num()
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func (ep *linkEndpoint) injectInbound(p *packet.Parsed) {
|
||||
@@ -198,6 +250,43 @@ func (ep *linkEndpoint) injectInbound(p *packet.Parsed) {
|
||||
pkt.DecRef()
|
||||
}
|
||||
|
||||
// DeliverLoopback delivers pkt back into gVisor's network stack as if it
|
||||
// arrived from the network, for self-addressed (loopback) packets. It takes
|
||||
// ownership of one reference count on pkt. The caller must not use pkt after
|
||||
// calling this method. It returns false if the dispatcher is not attached.
|
||||
//
|
||||
// Outbound packets from gVisor have their headers already parsed into separate
|
||||
// views (NetworkHeader, TransportHeader, Data). DeliverNetworkPacket expects
|
||||
// a raw unparsed packet, so we must re-serialize the packet into a new
|
||||
// PacketBuffer with all bytes in the payload for gVisor to parse on inbound.
|
||||
func (ep *linkEndpoint) DeliverLoopback(pkt *stack.PacketBuffer) bool {
|
||||
ep.mu.RLock()
|
||||
d := ep.dispatcher
|
||||
ep.mu.RUnlock()
|
||||
if d == nil {
|
||||
pkt.DecRef()
|
||||
return false
|
||||
}
|
||||
|
||||
// Serialize the outbound packet back to raw bytes.
|
||||
raw := stack.PayloadSince(pkt.NetworkHeader()).AsSlice()
|
||||
proto := pkt.NetworkProtocolNumber
|
||||
|
||||
// We're done with the original outbound packet.
|
||||
pkt.DecRef()
|
||||
|
||||
// Create a new PacketBuffer from the raw bytes for inbound delivery.
|
||||
newPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
Payload: buffer.MakeWithData(raw),
|
||||
})
|
||||
newPkt.NetworkProtocolNumber = proto
|
||||
newPkt.RXChecksumValidated = true
|
||||
|
||||
d.DeliverNetworkPacket(proto, newPkt)
|
||||
newPkt.DecRef()
|
||||
return true
|
||||
}
|
||||
|
||||
// Attach saves the stack network-layer dispatcher for use later when packets
|
||||
// are injected.
|
||||
func (ep *linkEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
||||
@@ -266,7 +355,7 @@ func (ep *linkEndpoint) SetLinkAddress(addr tcpip.LinkAddress) {
|
||||
ep.linkAddr = addr
|
||||
}
|
||||
|
||||
// WritePackets stores outbound packets into the channel.
|
||||
// WritePackets routes outbound packets into the appropriate outbound [queue].
|
||||
// Multiple concurrent calls are permitted.
|
||||
func (ep *linkEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) {
|
||||
n := 0
|
||||
@@ -278,7 +367,15 @@ func (ep *linkEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Er
|
||||
// control MTU (and by effect TCP MSS in gVisor) we *shouldn't* expect to
|
||||
// ever overflow 128 slots (see wireguard-go/tun.ErrTooManySegments usage).
|
||||
for _, pkt := range pkts.AsSlice() {
|
||||
if err := ep.q.Write(pkt); err != nil {
|
||||
q := ep.outboundQueueRouter(pkt)
|
||||
if q == outboundDrop {
|
||||
n++
|
||||
continue
|
||||
}
|
||||
if q < 0 || q >= outboundQueueLimit {
|
||||
panic(fmt.Sprintf("linkEndpoint.outboundQueueRouter returned %v which is outside outboundQueueLimit(%v)", q, outboundQueueLimit))
|
||||
}
|
||||
if err := ep.outboundQueues[q].Write(pkt); err != nil {
|
||||
if _, ok := err.(*tcpip.ErrNoBufferSpace); !ok && n == 0 {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
+566
-185
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package netstack
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !darwin && !ios
|
||||
|
||||
+17
-20
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build darwin || ios
|
||||
@@ -6,33 +6,30 @@
|
||||
package netstack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
probing "github.com/prometheus-community/pro-bing"
|
||||
"tailscale.com/net/ping"
|
||||
)
|
||||
|
||||
// sendOutboundUserPing sends a non-privileged ICMP (or ICMPv6) ping to dstIP with the given timeout.
|
||||
func (ns *Impl) sendOutboundUserPing(dstIP netip.Addr, timeout time.Duration) error {
|
||||
p, err := probing.NewPinger(dstIP.String())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
p := ping.New(ctx, ns.logf, nil)
|
||||
p.Unprivileged = true
|
||||
defer p.Close()
|
||||
|
||||
dst := &net.IPAddr{IP: dstIP.AsSlice(), Zone: dstIP.Zone()}
|
||||
ns.logf("sendOutboundUserPing: forwarding ping to %s", dstIP)
|
||||
d, err := p.Send(ctx, dst, []byte("tailscale-userping"))
|
||||
if err != nil {
|
||||
ns.logf("sendICMPPingToIP failed to create pinger: %v", err)
|
||||
ns.logf("sendOutboundUserPing: ping to %s failed: %v", dstIP, err)
|
||||
return err
|
||||
}
|
||||
|
||||
p.Timeout = timeout
|
||||
p.Count = 1
|
||||
p.SetPrivileged(false)
|
||||
|
||||
p.OnSend = func(pkt *probing.Packet) {
|
||||
ns.logf("sendICMPPingToIP: forwarding ping to %s:", p.Addr())
|
||||
}
|
||||
p.OnRecv = func(pkt *probing.Packet) {
|
||||
ns.logf("sendICMPPingToIP: %d bytes pong from %s: icmp_seq=%d time=%v", pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
|
||||
}
|
||||
p.OnFinish = func(stats *probing.Statistics) {
|
||||
ns.logf("sendICMPPingToIP: done, %d replies received", stats.PacketsRecv)
|
||||
}
|
||||
|
||||
return p.Run()
|
||||
ns.logf("sendOutboundUserPing: pong from %s in %v", dstIP, d)
|
||||
return nil
|
||||
}
|
||||
+78
-37
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_debug
|
||||
@@ -68,7 +68,6 @@ func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.Wrapp
|
||||
res = filter.Accept // always
|
||||
|
||||
if pp.IPProto == ipproto.TSMP {
|
||||
res = filter.DropSilently
|
||||
rh, ok := pp.AsTailscaleRejectedHeader()
|
||||
if !ok {
|
||||
return
|
||||
@@ -78,6 +77,15 @@ func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.Wrapp
|
||||
} else if f := tsRejectFlow(rh); e.removeFlow(f) {
|
||||
e.logf("open-conn-track: flow %v %v > %v rejected due to %v", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
e.notifyConnRejectTSMPRecv(rh)
|
||||
switch rh.Reason {
|
||||
case packet.RejectedDueToUnknownAppConnectorTransitIP:
|
||||
// Keep res = filter.Accept, don't drop this packet because it will
|
||||
// be used later for further communication between app connector
|
||||
// and client.
|
||||
default:
|
||||
res = filter.DropSilently
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,8 +109,8 @@ var (
|
||||
appleIPRange = netip.MustParsePrefix("17.0.0.0/8")
|
||||
canonicalIPs = sync.OnceValue(func() (checkIPFunc func(netip.Addr) bool) {
|
||||
// https://bgp.he.net/AS41231#_prefixes
|
||||
t := &bart.Table[bool]{}
|
||||
for _, s := range strings.Fields(`
|
||||
t := &bart.Lite{}
|
||||
for s := range strings.FieldsSeq(`
|
||||
91.189.89.0/24
|
||||
91.189.91.0/24
|
||||
91.189.92.0/24
|
||||
@@ -115,12 +123,9 @@ var (
|
||||
185.125.188.0/23
|
||||
185.125.190.0/24
|
||||
194.169.254.0/24`) {
|
||||
t.Insert(netip.MustParsePrefix(s), true)
|
||||
}
|
||||
return func(ip netip.Addr) bool {
|
||||
v, _ := t.Lookup(ip)
|
||||
return v
|
||||
t.Insert(netip.MustParsePrefix(s))
|
||||
}
|
||||
return t.Contains
|
||||
})
|
||||
)
|
||||
|
||||
@@ -134,14 +139,14 @@ func (e *userspaceEngine) isOSNetworkProbe(dst netip.AddrPort) bool {
|
||||
// iOS had log spam like:
|
||||
// open-conn-track: timeout opening (100.115.73.60:52501 => 17.125.252.5:443); no associated peer node
|
||||
if runtime.GOOS == "ios" && dst.Port() == 443 && appleIPRange.Contains(dst.Addr()) {
|
||||
if _, ok := e.PeerForIP(dst.Addr()); !ok {
|
||||
if _, ok := e.peerForIP(dst.Addr()); !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// NetworkManager; https://github.com/tailscale/tailscale/issues/13687
|
||||
// open-conn-track: timeout opening (TCP 100.96.229.119:42798 => 185.125.190.49:80); no associated peer node
|
||||
if runtime.GOOS == "linux" && dst.Port() == 80 && canonicalIPs()(dst.Addr()) {
|
||||
if _, ok := e.PeerForIP(dst.Addr()); !ok {
|
||||
if _, ok := e.peerForIP(dst.Addr()); !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -178,37 +183,56 @@ func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.Wra
|
||||
return filter.Accept
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
e.mu.Lock()
|
||||
of, ok := e.pendOpen[flow]
|
||||
if !ok {
|
||||
// Not a tracked flow, or already handled & deleted.
|
||||
e.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(e.pendOpen, flow)
|
||||
problem := of.problem
|
||||
e.mu.Unlock()
|
||||
// openTimeoutDiag captures what [userspaceEngine.diagnoseOpenTimeout]
|
||||
// inferred about a pendopen flow that timed out. It is the input to
|
||||
// [classifyOpenTimeout].
|
||||
type openTimeoutDiag struct {
|
||||
// problem is the TSMP reject reason previously reported by the peer
|
||||
// via a MaybeBroken TSMP header, or zero if none.
|
||||
problem packet.TailscaleRejectReason
|
||||
|
||||
// noPeer is true if PeerForIP did not find a peer for the flow.
|
||||
noPeer bool
|
||||
// peerUnreachable is true when we identified a Tailscale peer for
|
||||
// the flow but it cannot currently be reached (pre-0.100 client,
|
||||
// no HomeDERP, in-netmap-but-unknown-to-WireGuard, etc.).
|
||||
peerUnreachable bool
|
||||
// onlyZeroRoute is true if PeerForIP matched the peer only via a
|
||||
// /0 route (likely a non-selected exit node). The outer code
|
||||
// deliberately stays silent in this case; classification would be
|
||||
// misleading.
|
||||
onlyZeroRoute bool
|
||||
}
|
||||
|
||||
// diagnoseOpenTimeout examines the engine state and logs a description
|
||||
// of why a pendopen flow timed out. It returns the populated
|
||||
// [openTimeoutDiag] so the caller can classify and (optionally) emit a
|
||||
// connreject event.
|
||||
func (e *userspaceEngine) diagnoseOpenTimeout(flow flowtrack.Tuple, problem packet.TailscaleRejectReason) openTimeoutDiag {
|
||||
d := openTimeoutDiag{problem: problem}
|
||||
|
||||
if !problem.IsZero() {
|
||||
e.logf("open-conn-track: timeout opening %v; peer reported problem: %v", flow, problem)
|
||||
}
|
||||
|
||||
// Diagnose why it might've timed out.
|
||||
pip, ok := e.PeerForIP(flow.DstAddr())
|
||||
pip, ok := e.peerForIP(flow.DstAddr())
|
||||
if !ok {
|
||||
e.logf("open-conn-track: timeout opening %v; no associated peer node", flow)
|
||||
return
|
||||
d.noPeer = true
|
||||
return d
|
||||
}
|
||||
n := pip.Node
|
||||
if !n.IsWireGuardOnly() {
|
||||
if n.DiscoKey().IsZero() {
|
||||
e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key().ShortString())
|
||||
return
|
||||
d.peerUnreachable = true
|
||||
return d
|
||||
}
|
||||
if n.HomeDERP() == 0 {
|
||||
e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key().ShortString())
|
||||
return
|
||||
d.peerUnreachable = true
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,19 +246,19 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
}
|
||||
}
|
||||
if onlyZeroRoute {
|
||||
// This node was returned by peerForIP because
|
||||
// its exit node /0 route(s) matched, but this
|
||||
// might not be the exit node that's currently
|
||||
// selected. Rather than log misleading
|
||||
// errors, just don't log at all for now.
|
||||
// TODO(bradfitz): update this code to be
|
||||
// exit-node-aware and make peerForIP return
|
||||
// the node of the currently selected exit
|
||||
// node.
|
||||
return
|
||||
// This node was returned by peerForIP because its exit
|
||||
// node /0 route(s) matched, but this might not be the
|
||||
// exit node that's currently selected. Rather than log
|
||||
// misleading errors, just don't log at all for now.
|
||||
// TODO(bradfitz): update this code to be exit-node-aware
|
||||
// and make peerForIP return the node of the currently
|
||||
// selected exit node.
|
||||
d.onlyZeroRoute = true
|
||||
return d
|
||||
}
|
||||
e.logf("open-conn-track: timeout opening %v; target node %v in netmap but unknown to WireGuard", flow, n.Key().ShortString())
|
||||
return
|
||||
d.peerUnreachable = true
|
||||
return d
|
||||
}
|
||||
|
||||
// TODO(bradfitz): figure out what PeerStatus.LastHandshake
|
||||
@@ -262,6 +286,23 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
flow, n.Key().ShortString(),
|
||||
online,
|
||||
e.magicConn.LastRecvActivityOfNodeKey(n.Key()))
|
||||
return d
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
e.mu.Lock()
|
||||
of, ok := e.pendOpen[flow]
|
||||
if !ok {
|
||||
// Not a tracked flow, or already handled & deleted.
|
||||
e.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(e.pendOpen, flow)
|
||||
problem := of.problem
|
||||
e.mu.Unlock()
|
||||
|
||||
d := e.diagnoseOpenTimeout(flow, problem)
|
||||
e.notifyConnRejectOpenTimeout(flow, d)
|
||||
}
|
||||
|
||||
func durFmt(t time.Time) string {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_debug
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package router
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package router
|
||||
|
||||
+9
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package router presents an interface to manipulate the host network
|
||||
@@ -131,11 +131,15 @@ type Config struct {
|
||||
// flow logging and is otherwise ignored.
|
||||
SubnetRoutes []netip.Prefix
|
||||
|
||||
// SNATSubnetRoutes enables SNAT for traffic to local subnets.
|
||||
// Implemented on Linux (iptables/nftables) and FreeBSD (PF).
|
||||
SNATSubnetRoutes bool
|
||||
|
||||
// Linux-only things below, ignored on other platforms.
|
||||
SNATSubnetRoutes bool // SNAT traffic to local subnets
|
||||
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||
NetfilterKind string // what kind of netfilter to use ("nftables", "iptables", or "" to auto-detect)
|
||||
StatefulFiltering bool // Apply stateful filtering to inbound connections
|
||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||
NetfilterKind string // what kind of netfilter to use ("nftables", "iptables", or "" to auto-detect)
|
||||
RemoveCGNATDropRule bool // whether to remove the firewall rule to drop non-Tailscale inbound traffic from CGNAT IPs
|
||||
}
|
||||
|
||||
func (a *Config) Equal(b *Config) bool {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package router
|
||||
|
||||
+403
-640
File diff suppressed because it is too large.
Load diff
-181
@@ -1,181 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !js && !ts_omit_debug
|
||||
|
||||
package wgengine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wgint"
|
||||
)
|
||||
|
||||
// NewWatchdog wraps an Engine and makes sure that all methods complete
|
||||
// within a reasonable amount of time.
|
||||
//
|
||||
// If they do not, the watchdog crashes the process.
|
||||
func NewWatchdog(e Engine) Engine {
|
||||
if envknob.Bool("TS_DEBUG_DISABLE_WATCHDOG") {
|
||||
return e
|
||||
}
|
||||
return &watchdogEngine{
|
||||
wrap: e,
|
||||
logf: log.Printf,
|
||||
fatalf: log.Fatalf,
|
||||
maxWait: 45 * time.Second,
|
||||
inFlight: make(map[inFlightKey]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
type inFlightKey struct {
|
||||
op string
|
||||
ctr uint64
|
||||
}
|
||||
|
||||
type watchdogEngine struct {
|
||||
wrap Engine
|
||||
logf func(format string, args ...any)
|
||||
fatalf func(format string, args ...any)
|
||||
maxWait time.Duration
|
||||
|
||||
// Track the start time(s) of in-flight operations
|
||||
inFlightMu sync.Mutex
|
||||
inFlight map[inFlightKey]time.Time
|
||||
inFlightCtr uint64
|
||||
}
|
||||
|
||||
func (e *watchdogEngine) watchdogErr(name string, fn func() error) error {
|
||||
// Track all in-flight operations so we can print more useful error
|
||||
// messages on watchdog failure
|
||||
e.inFlightMu.Lock()
|
||||
key := inFlightKey{
|
||||
op: name,
|
||||
ctr: e.inFlightCtr,
|
||||
}
|
||||
e.inFlightCtr++
|
||||
e.inFlight[key] = time.Now()
|
||||
e.inFlightMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
e.inFlightMu.Lock()
|
||||
defer e.inFlightMu.Unlock()
|
||||
delete(e.inFlight, key)
|
||||
}()
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
errCh <- fn()
|
||||
}()
|
||||
t := time.NewTimer(e.maxWait)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
t.Stop()
|
||||
return err
|
||||
case <-t.C:
|
||||
buf := new(strings.Builder)
|
||||
pprof.Lookup("goroutine").WriteTo(buf, 1)
|
||||
e.logf("wgengine watchdog stacks:\n%s", buf.String())
|
||||
|
||||
// Collect the list of in-flight operations for debugging.
|
||||
var (
|
||||
b []byte
|
||||
now = time.Now()
|
||||
)
|
||||
e.inFlightMu.Lock()
|
||||
for k, t := range e.inFlight {
|
||||
dur := now.Sub(t).Round(time.Millisecond)
|
||||
b = fmt.Appendf(b, "in-flight[%d]: name=%s duration=%v start=%s\n", k.ctr, k.op, dur, t.Format(time.RFC3339Nano))
|
||||
}
|
||||
e.inFlightMu.Unlock()
|
||||
|
||||
// Print everything as a single string to avoid log
|
||||
// rate limits.
|
||||
e.logf("wgengine watchdog in-flight:\n%s", b)
|
||||
e.fatalf("wgengine: watchdog timeout on %s", name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (e *watchdogEngine) watchdog(name string, fn func()) {
|
||||
e.watchdogErr(name, func() error {
|
||||
fn()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (e *watchdogEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config) error {
|
||||
return e.watchdogErr("Reconfig", func() error { return e.wrap.Reconfig(cfg, routerCfg, dnsCfg) })
|
||||
}
|
||||
func (e *watchdogEngine) ResetAndStop() (st *Status, err error) {
|
||||
e.watchdog("ResetAndStop", func() {
|
||||
st, err = e.wrap.ResetAndStop()
|
||||
})
|
||||
return st, err
|
||||
}
|
||||
func (e *watchdogEngine) GetFilter() *filter.Filter {
|
||||
return e.wrap.GetFilter()
|
||||
}
|
||||
func (e *watchdogEngine) SetFilter(filt *filter.Filter) {
|
||||
e.watchdog("SetFilter", func() { e.wrap.SetFilter(filt) })
|
||||
}
|
||||
func (e *watchdogEngine) GetJailedFilter() *filter.Filter {
|
||||
return e.wrap.GetJailedFilter()
|
||||
}
|
||||
func (e *watchdogEngine) SetJailedFilter(filt *filter.Filter) {
|
||||
e.watchdog("SetJailedFilter", func() { e.wrap.SetJailedFilter(filt) })
|
||||
}
|
||||
func (e *watchdogEngine) SetStatusCallback(cb StatusCallback) {
|
||||
e.watchdog("SetStatusCallback", func() { e.wrap.SetStatusCallback(cb) })
|
||||
}
|
||||
func (e *watchdogEngine) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
e.watchdog("UpdateStatus", func() { e.wrap.UpdateStatus(sb) })
|
||||
}
|
||||
func (e *watchdogEngine) RequestStatus() {
|
||||
e.watchdog("RequestStatus", func() { e.wrap.RequestStatus() })
|
||||
}
|
||||
func (e *watchdogEngine) SetNetworkMap(nm *netmap.NetworkMap) {
|
||||
e.watchdog("SetNetworkMap", func() { e.wrap.SetNetworkMap(nm) })
|
||||
}
|
||||
func (e *watchdogEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult)) {
|
||||
e.watchdog("Ping", func() { e.wrap.Ping(ip, pingType, size, cb) })
|
||||
}
|
||||
func (e *watchdogEngine) Close() {
|
||||
e.watchdog("Close", e.wrap.Close)
|
||||
}
|
||||
func (e *watchdogEngine) PeerForIP(ip netip.Addr) (ret PeerForIP, ok bool) {
|
||||
e.watchdog("PeerForIP", func() { ret, ok = e.wrap.PeerForIP(ip) })
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (e *watchdogEngine) Done() <-chan struct{} {
|
||||
return e.wrap.Done()
|
||||
}
|
||||
|
||||
func (e *watchdogEngine) InstallCaptureHook(cb packet.CaptureCallback) {
|
||||
if !buildfeatures.HasCapture {
|
||||
return
|
||||
}
|
||||
e.wrap.InstallCaptureHook(cb)
|
||||
}
|
||||
|
||||
func (e *watchdogEngine) PeerByKey(pubKey key.NodePublic) (_ wgint.Peer, ok bool) {
|
||||
return e.wrap.PeerByKey(pubKey)
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build js || ts_omit_debug
|
||||
|
||||
package wgengine
|
||||
|
||||
func NewWatchdog(e Engine) Engine { return e }
|
||||
+17
-62
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package wgcfg has types and a parser for representing WireGuard config.
|
||||
@@ -8,29 +8,31 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logid"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Config,Peer
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Config
|
||||
|
||||
// Config is a WireGuard configuration.
|
||||
// It only supports the set of things Tailscale uses.
|
||||
//
|
||||
// Peers are not part of the config: wireguard-go learns the peer set
|
||||
// and each peer's configuration from the live per-peer config source
|
||||
// installed via [tailscale.com/wgengine.Engine.SetPeerConfigFunc].
|
||||
type Config struct {
|
||||
PrivateKey key.NodePrivate
|
||||
Addresses []netip.Prefix
|
||||
MTU uint16
|
||||
DNS []netip.Addr
|
||||
Peers []Peer
|
||||
}
|
||||
|
||||
// NetworkLogging enables network logging.
|
||||
// It is disabled if either ID is the zero value.
|
||||
// LogExitFlowEnabled indicates whether or not exit flows should be logged.
|
||||
NetworkLogging struct {
|
||||
NodeID logid.PrivateID
|
||||
DomainID logid.PrivateID
|
||||
LogExitFlowEnabled bool
|
||||
}
|
||||
// PeerConfig is the WireGuard configuration for one peer.
|
||||
type PeerConfig struct {
|
||||
// AllowedIPs is the set of prefixes the peer may originate traffic from.
|
||||
AllowedIPs []netip.Prefix
|
||||
|
||||
// PresharedKey is the optional WireGuard pre-shared key. The zero value
|
||||
// disables the pre-shared-key layer.
|
||||
PresharedKey device.NoisePresharedKey
|
||||
}
|
||||
|
||||
func (c *Config) Equal(o *Config) bool {
|
||||
@@ -38,52 +40,5 @@ func (c *Config) Equal(o *Config) bool {
|
||||
return c == o
|
||||
}
|
||||
return c.PrivateKey.Equal(o.PrivateKey) &&
|
||||
c.MTU == o.MTU &&
|
||||
c.NetworkLogging == o.NetworkLogging &&
|
||||
slices.Equal(c.Addresses, o.Addresses) &&
|
||||
slices.Equal(c.DNS, o.DNS) &&
|
||||
slices.EqualFunc(c.Peers, o.Peers, Peer.Equal)
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
PublicKey key.NodePublic
|
||||
DiscoKey key.DiscoPublic // present only so we can handle restarts within wgengine, not passed to WireGuard
|
||||
AllowedIPs []netip.Prefix
|
||||
V4MasqAddr *netip.Addr // if non-nil, masquerade IPv4 traffic to this peer using this address
|
||||
V6MasqAddr *netip.Addr // if non-nil, masquerade IPv6 traffic to this peer using this address
|
||||
IsJailed bool // if true, this peer is jailed and cannot initiate connections
|
||||
PersistentKeepalive uint16 // in seconds between keep-alives; 0 to disable
|
||||
// wireguard-go's endpoint for this peer. It should always equal Peer.PublicKey.
|
||||
// We represent it explicitly so that we can detect if they diverge and recover.
|
||||
// There is no need to set WGEndpoint explicitly when constructing a Peer by hand.
|
||||
// It is only populated when reading Peers from wireguard-go.
|
||||
WGEndpoint key.NodePublic
|
||||
}
|
||||
|
||||
func addrPtrEq(a, b *netip.Addr) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func (p Peer) Equal(o Peer) bool {
|
||||
return p.PublicKey == o.PublicKey &&
|
||||
p.DiscoKey == o.DiscoKey &&
|
||||
slices.Equal(p.AllowedIPs, o.AllowedIPs) &&
|
||||
p.IsJailed == o.IsJailed &&
|
||||
p.PersistentKeepalive == o.PersistentKeepalive &&
|
||||
addrPtrEq(p.V4MasqAddr, o.V4MasqAddr) &&
|
||||
addrPtrEq(p.V6MasqAddr, o.V6MasqAddr) &&
|
||||
p.WGEndpoint == o.WGEndpoint
|
||||
}
|
||||
|
||||
// PeerWithKey returns the Peer with key k and reports whether it was found.
|
||||
func (config Config) PeerWithKey(k key.NodePublic) (Peer, bool) {
|
||||
for _, p := range config.Peers {
|
||||
if p.PublicKey == k {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return Peer{}, false
|
||||
slices.Equal(c.Addresses, o.Addresses)
|
||||
}
|
||||
+42
-46
@@ -1,68 +1,64 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sort"
|
||||
"fmt"
|
||||
|
||||
"github.com/tailscale/wireguard-go/conn"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
|
||||
// NewDevice returns a wireguard-go Device configured for Tailscale use.
|
||||
func NewDevice(tunDev tun.Device, bind conn.Bind, logger *device.Logger) *device.Device {
|
||||
ret := device.NewDevice(tunDev, bind, logger)
|
||||
ret.DisableSomeRoamingForBrokenMobileSemantics()
|
||||
return ret
|
||||
return device.NewDevice(tunDev, bind, logger, append(getMemoryOptions(), getDeviceMetrics())...)
|
||||
}
|
||||
|
||||
func DeviceConfig(d *device.Device) (*Config, error) {
|
||||
r, w := io.Pipe()
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
errc <- d.IpcGetOperation(w)
|
||||
w.Close()
|
||||
}()
|
||||
cfg, fromErr := FromUAPI(r)
|
||||
r.Close()
|
||||
getErr := <-errc
|
||||
err := errors.Join(getErr, fromErr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(cfg.Peers, func(i, j int) bool {
|
||||
return cfg.Peers[i].PublicKey.Less(cfg.Peers[j].PublicKey)
|
||||
func getDeviceMetrics() device.Option {
|
||||
return device.WithMetrics(device.Metrics{
|
||||
MessageInitiationTXAttemptInitial: metricMessageInitiationTXAttemptInitial,
|
||||
MessageInitiationTXAttemptRetry: metricMessageInitiationTXAttemptRetry,
|
||||
MessageResponseTXAttempt: metricMessageResponseTXAttempt,
|
||||
MessageCookieReplyTXAttempt: metricMessageCookieReplyTXAttempt,
|
||||
HandshakeInitiatorCompleted: metricHandshakeInitiatorCompleted,
|
||||
HandshakeResponderCompleted: metricHandshakeResponderCompleted,
|
||||
MessageTransportRXDroppedReplay: metricMessageTransportRXDroppedReplay,
|
||||
})
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// ReconfigDevice replaces the existing device configuration with cfg.
|
||||
func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logf("wgcfg.Reconfig failed: %v", err)
|
||||
var (
|
||||
metricMessageInitiationTXAttemptInitial = clientmetric.NewCounter("wireguard_message_initiation_tx_attempt_initial")
|
||||
metricMessageInitiationTXAttemptRetry = clientmetric.NewCounter("wireguard_message_initiation_tx_attempt_retry")
|
||||
metricMessageResponseTXAttempt = clientmetric.NewCounter("wireguard_message_response_tx_attempt")
|
||||
metricMessageCookieReplyTXAttempt = clientmetric.NewCounter("wireguard_message_cookie_reply_tx_attempt")
|
||||
metricHandshakeInitiatorCompleted = clientmetric.NewCounter("wireguard_handshake_initiator_completed")
|
||||
metricHandshakeResponderCompleted = clientmetric.NewCounter("wireguard_handshake_responder_completed")
|
||||
metricMessageTransportRXDroppedReplay = clientmetric.NewCounter("wireguard_message_transport_rx_dropped_replay")
|
||||
)
|
||||
|
||||
// NewPeerLookupFunc returns a [device.PeerLookupFunc] that lazily
|
||||
// creates peers using peerConfig as the source of each peer's allowed IPs and
|
||||
// optional pre-shared key. The peer's endpoint is derived from its public key
|
||||
// via bind.
|
||||
func NewPeerLookupFunc(bind conn.Bind, logf logger.Logf, peerConfig func(device.NoisePublicKey) (PeerConfig, bool)) device.PeerLookupFunc {
|
||||
return func(pubk device.NoisePublicKey) (_ *device.NewPeerConfig, ok bool) {
|
||||
conf, ok := peerConfig(pubk)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}()
|
||||
|
||||
prev, err := DeviceConfig(d)
|
||||
if err != nil {
|
||||
return err
|
||||
ep, err := bind.ParseEndpoint(fmt.Sprintf("%x", pubk[:]))
|
||||
if err != nil {
|
||||
logf("wgcfg: failed to parse endpoint for peer %x: %v", pubk[:8], err)
|
||||
return nil, false
|
||||
}
|
||||
return &device.NewPeerConfig{
|
||||
AllowedIPs: conf.AllowedIPs,
|
||||
PresharedKey: conf.PresharedKey,
|
||||
Endpoint: ep,
|
||||
}, true
|
||||
}
|
||||
|
||||
r, w := io.Pipe()
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
errc <- d.IpcSetOperation(r)
|
||||
r.Close()
|
||||
}()
|
||||
|
||||
toErr := cfg.ToUAPI(logf, w, prev)
|
||||
w.Close()
|
||||
setErr := <-errc
|
||||
return errors.Join(setErr, toErr)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
)
|
||||
|
||||
func getMemoryOptions() []device.Option {
|
||||
// iOS has a very restrictive memory limit on network extensions.
|
||||
// Reduce the maximum amount of memory that wireguard-go can allocate to
|
||||
// avoid getting killed.
|
||||
//
|
||||
// See _notios.go for a longer explanation of what the Queue{In,Out}boundSize
|
||||
// knobs control and the tradeoffs involved. Eventually Queue{In,Out}boundSize
|
||||
// should be CPU-scaled, and WithPreallocated... removed, but we keep them all
|
||||
// static for now, until the [device.WaitPool] ceiling is eliminated.
|
||||
// (See tailscale/corp#46396).
|
||||
return []device.Option{
|
||||
device.WithQueueStagedSize(64),
|
||||
device.WithQueueOutboundSize(64),
|
||||
device.WithQueueInboundSize(64),
|
||||
device.WithQueueHandshakeSize(64),
|
||||
device.WithPreallocatedBuffersPerPool(64),
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
)
|
||||
|
||||
func getMemoryOptions() []device.Option {
|
||||
// wireguard-go is a packet pipeline:
|
||||
//
|
||||
// TUN read -> encrypt -> ordered network write
|
||||
// network read -> decrypt -> ordered TUN write
|
||||
//
|
||||
// The queues that we are sizing in this function sit between head I/O
|
||||
// goroutines and downstream functions. They are buffered channels.
|
||||
//
|
||||
// The head I/O ops tend to "run ahead" the downstream functions, especially
|
||||
// when head I/O ops utilize segmentation offloading, where a single head I/O
|
||||
// op can contain about 45 packets at a typical MTU, which is equivalent to
|
||||
// 45 crypto ops.
|
||||
//
|
||||
// Small queues park I/O goroutines and hurt throughput; large queues allow
|
||||
// more packet batches and their associated memory to remain outstanding. We
|
||||
// have to find a balance. Crypto goroutines are sized to [runtime.NumCPU]
|
||||
// in wireguard-go, so we start there, and apply a benchmark-backed scaling
|
||||
// factor to reduce head I/O goroutines parking around bursts.
|
||||
const cpuScalingFactor = 4
|
||||
// maxQueueSize was the previous static default, prior to creation of the
|
||||
// [device.Option].
|
||||
const maxQueueSize = 1024
|
||||
return []device.Option{
|
||||
// TODO: make cgroup-aware with runtime.GOMAXPROCS(0).
|
||||
// See tailscale/corp#47966.
|
||||
device.WithQueueInboundSize(min(maxQueueSize, runtime.NumCPU()*cpuScalingFactor)),
|
||||
device.WithQueueOutboundSize(min(maxQueueSize, runtime.NumCPU()*cpuScalingFactor)),
|
||||
}
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package nmcfg converts a controlclient.NetMap into a wgcfg config.
|
||||
package nmcfg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func nodeDebugName(n tailcfg.NodeView) string {
|
||||
name, _, _ := strings.Cut(cmp.Or(n.Name(), n.Hostinfo().Hostname()), ".")
|
||||
return name
|
||||
}
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node tailcfg.NodeView, cidr netip.Prefix) bool {
|
||||
if cidr.Bits() == 0 {
|
||||
return false
|
||||
}
|
||||
if !cidr.IsSingleIP() {
|
||||
return true
|
||||
}
|
||||
if tsaddr.IsTailscaleIP(cidr.Addr()) {
|
||||
return false
|
||||
}
|
||||
for _, selfCIDR := range node.Addresses().All() {
|
||||
if cidr == selfCIDR {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WGCfg returns the NetworkMaps's WireGuard configuration.
|
||||
func WGCfg(pk key.NodePrivate, nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, exitNode tailcfg.StableNodeID) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
PrivateKey: pk,
|
||||
Addresses: nm.GetAddresses().AsSlice(),
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
}
|
||||
|
||||
// Setup log IDs for data plane audit logging.
|
||||
if nm.SelfNode.Valid() {
|
||||
canNetworkLog := nm.SelfNode.HasCap(tailcfg.CapabilityDataPlaneAuditLogs)
|
||||
logExitFlowEnabled := nm.SelfNode.HasCap(tailcfg.NodeAttrLogExitFlows)
|
||||
if canNetworkLog && nm.SelfNode.DataPlaneAuditLogID() != "" && nm.DomainAuditLogID != "" {
|
||||
nodeID, errNode := logid.ParsePrivateID(nm.SelfNode.DataPlaneAuditLogID())
|
||||
if errNode != nil {
|
||||
logf("[v1] wgcfg: unable to parse node audit log ID: %v", errNode)
|
||||
}
|
||||
domainID, errDomain := logid.ParsePrivateID(nm.DomainAuditLogID)
|
||||
if errDomain != nil {
|
||||
logf("[v1] wgcfg: unable to parse domain audit log ID: %v", errDomain)
|
||||
}
|
||||
if errNode == nil && errDomain == nil {
|
||||
cfg.NetworkLogging.NodeID = nodeID
|
||||
cfg.NetworkLogging.DomainID = domainID
|
||||
cfg.NetworkLogging.LogExitFlowEnabled = logExitFlowEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var skippedExitNode, skippedSubnetRouter, skippedExpired []tailcfg.NodeView
|
||||
|
||||
for _, peer := range nm.Peers {
|
||||
if peer.DiscoKey().IsZero() && peer.HomeDERP() == 0 && !peer.IsWireGuardOnly() {
|
||||
// Peer predates both DERP and active discovery, we cannot
|
||||
// communicate with it.
|
||||
logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key().ShortString())
|
||||
continue
|
||||
}
|
||||
// Skip expired peers; we'll end up failing to connect to them
|
||||
// anyway, since control intentionally breaks node keys for
|
||||
// expired peers so that we can't discover endpoints via DERP.
|
||||
if peer.Expired() {
|
||||
skippedExpired = append(skippedExpired, peer)
|
||||
continue
|
||||
}
|
||||
|
||||
cfg.Peers = append(cfg.Peers, wgcfg.Peer{
|
||||
PublicKey: peer.Key(),
|
||||
DiscoKey: peer.DiscoKey(),
|
||||
})
|
||||
cpeer := &cfg.Peers[len(cfg.Peers)-1]
|
||||
|
||||
didExitNodeLog := false
|
||||
cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer().Clone()
|
||||
cpeer.V6MasqAddr = peer.SelfNodeV6MasqAddrForThisPeer().Clone()
|
||||
cpeer.IsJailed = peer.IsJailed()
|
||||
for _, allowedIP := range peer.AllowedIPs().All() {
|
||||
if allowedIP.Bits() == 0 && peer.StableID() != exitNode {
|
||||
if didExitNodeLog {
|
||||
// Don't log about both the IPv4 /0 and IPv6 /0.
|
||||
continue
|
||||
}
|
||||
didExitNodeLog = true
|
||||
skippedExitNode = append(skippedExitNode, peer)
|
||||
continue
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & netmap.AllowSubnetRoutes) == 0 {
|
||||
skippedSubnetRouter = append(skippedSubnetRouter, peer)
|
||||
continue
|
||||
}
|
||||
}
|
||||
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
|
||||
}
|
||||
}
|
||||
|
||||
logList := func(title string, nodes []tailcfg.NodeView) {
|
||||
if len(nodes) == 0 {
|
||||
return
|
||||
}
|
||||
logf("[v1] wgcfg: %s from %d nodes: %s", title, len(nodes), logger.ArgWriter(func(bw *bufio.Writer) {
|
||||
const max = 5
|
||||
for i, n := range nodes {
|
||||
if i == max {
|
||||
fmt.Fprintf(bw, "... +%d", len(nodes)-max)
|
||||
return
|
||||
}
|
||||
if i > 0 {
|
||||
bw.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(bw, "%s (%s)", nodeDebugName(n), n.StableID())
|
||||
}
|
||||
}))
|
||||
}
|
||||
logList("skipped unselected exit nodes", skippedExitNode)
|
||||
logList("did not accept subnet routes", skippedSubnetRouter)
|
||||
logList("skipped expired peers", skippedExpired)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
type ParseError struct {
|
||||
why string
|
||||
offender string
|
||||
}
|
||||
|
||||
func (e *ParseError) Error() string {
|
||||
return fmt.Sprintf("%s: %q", e.why, e.offender)
|
||||
}
|
||||
|
||||
func parseEndpoint(s string) (host string, port uint16, err error) {
|
||||
i := strings.LastIndexByte(s, ':')
|
||||
if i < 0 {
|
||||
return "", 0, &ParseError{"Missing port from endpoint", s}
|
||||
}
|
||||
host, portStr := s[:i], s[i+1:]
|
||||
if len(host) < 1 {
|
||||
return "", 0, &ParseError{"Invalid endpoint host", host}
|
||||
}
|
||||
uport, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
hostColon := strings.IndexByte(host, ':')
|
||||
if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 {
|
||||
err := &ParseError{"Brackets must contain an IPv6 address", host}
|
||||
if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 {
|
||||
maybeV6 := net.ParseIP(host[1 : len(host)-1])
|
||||
if maybeV6 == nil || len(maybeV6) != net.IPv6len {
|
||||
return "", 0, err
|
||||
}
|
||||
} else {
|
||||
return "", 0, err
|
||||
}
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
return host, uint16(uport), nil
|
||||
}
|
||||
|
||||
// memROCut separates a mem.RO at the separator if it exists, otherwise
|
||||
// it returns two empty ROs and reports that it was not found.
|
||||
func memROCut(s mem.RO, sep byte) (before, after mem.RO, found bool) {
|
||||
if i := mem.IndexByte(s, sep); i >= 0 {
|
||||
return s.SliceTo(i), s.SliceFrom(i + 1), true
|
||||
}
|
||||
found = false
|
||||
return
|
||||
}
|
||||
|
||||
// FromUAPI generates a Config from r.
|
||||
// r should be generated by calling device.IpcGetOperation;
|
||||
// it is not compatible with other uapi streams.
|
||||
func FromUAPI(r io.Reader) (*Config, error) {
|
||||
cfg := new(Config)
|
||||
var peer *Peer // current peer being operated on
|
||||
deviceConfig := true
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := mem.B(scanner.Bytes())
|
||||
if line.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
key, value, ok := memROCut(line, '=')
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cut line %q on =", line.StringCopy())
|
||||
}
|
||||
valueBytes := scanner.Bytes()[key.Len()+1:]
|
||||
|
||||
if key.EqualString("public_key") {
|
||||
if deviceConfig {
|
||||
deviceConfig = false
|
||||
}
|
||||
// Load/create the peer we are now configuring.
|
||||
var err error
|
||||
peer, err = cfg.handlePublicKeyLine(valueBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
if deviceConfig {
|
||||
err = cfg.handleDeviceLine(key, value, valueBytes)
|
||||
} else {
|
||||
err = cfg.handlePeerLine(peer, key, value, valueBytes)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handleDeviceLine(k, value mem.RO, valueBytes []byte) error {
|
||||
switch {
|
||||
case k.EqualString("private_key"):
|
||||
// wireguard-go guarantees not to send zero value; private keys are already clamped.
|
||||
var err error
|
||||
cfg.PrivateKey, err = key.ParseNodePrivateUntyped(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case k.EqualString("listen_port") || k.EqualString("fwmark"):
|
||||
// ignore
|
||||
default:
|
||||
return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handlePublicKeyLine(valueBytes []byte) (*Peer, error) {
|
||||
p := Peer{}
|
||||
var err error
|
||||
p.PublicKey, err = key.ParseNodePublicUntyped(mem.B(valueBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Peers = append(cfg.Peers, p)
|
||||
return &cfg.Peers[len(cfg.Peers)-1], nil
|
||||
}
|
||||
|
||||
func (cfg *Config) handlePeerLine(peer *Peer, k, value mem.RO, valueBytes []byte) error {
|
||||
switch {
|
||||
case k.EqualString("endpoint"):
|
||||
nk, err := key.ParseNodePublicUntyped(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid endpoint %q for peer %q, expected a hex public key", value.StringCopy(), peer.PublicKey.ShortString())
|
||||
}
|
||||
// nk ought to equal peer.PublicKey.
|
||||
// Under some rare circumstances, it might not. See corp issue #3016.
|
||||
// Even if that happens, don't stop early, so that we can recover from it.
|
||||
// Instead, note the value of nk so we can fix as needed.
|
||||
peer.WGEndpoint = nk
|
||||
case k.EqualString("persistent_keepalive_interval"):
|
||||
n, err := mem.ParseUint(value, 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.PersistentKeepalive = uint16(n)
|
||||
case k.EqualString("allowed_ip"):
|
||||
ipp := netip.Prefix{}
|
||||
err := ipp.UnmarshalText(valueBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer.AllowedIPs = append(peer.AllowedIPs, ipp)
|
||||
case k.EqualString("protocol_version"):
|
||||
if !value.EqualString("1") {
|
||||
return fmt.Errorf("invalid protocol version: %q", value.StringCopy())
|
||||
}
|
||||
case k.EqualString("replace_allowed_ips") ||
|
||||
k.EqualString("preshared_key") ||
|
||||
k.EqualString("last_handshake_time_sec") ||
|
||||
k.EqualString("last_handshake_time_nsec") ||
|
||||
k.EqualString("tx_bytes") ||
|
||||
k.EqualString("rx_bytes"):
|
||||
// ignore
|
||||
default:
|
||||
return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+3
-50
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Config.
|
||||
@@ -22,56 +20,11 @@ func (src *Config) Clone() *Config {
|
||||
dst := new(Config)
|
||||
*dst = *src
|
||||
dst.Addresses = append(src.Addresses[:0:0], src.Addresses...)
|
||||
dst.DNS = append(src.DNS[:0:0], src.DNS...)
|
||||
if src.Peers != nil {
|
||||
dst.Peers = make([]Peer, len(src.Peers))
|
||||
for i := range dst.Peers {
|
||||
dst.Peers[i] = *src.Peers[i].Clone()
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _ConfigCloneNeedsRegeneration = Config(struct {
|
||||
PrivateKey key.NodePrivate
|
||||
Addresses []netip.Prefix
|
||||
MTU uint16
|
||||
DNS []netip.Addr
|
||||
Peers []Peer
|
||||
NetworkLogging struct {
|
||||
NodeID logid.PrivateID
|
||||
DomainID logid.PrivateID
|
||||
LogExitFlowEnabled bool
|
||||
}
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Peer.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Peer) Clone() *Peer {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Peer)
|
||||
*dst = *src
|
||||
dst.AllowedIPs = append(src.AllowedIPs[:0:0], src.AllowedIPs...)
|
||||
if dst.V4MasqAddr != nil {
|
||||
dst.V4MasqAddr = ptr.To(*src.V4MasqAddr)
|
||||
}
|
||||
if dst.V6MasqAddr != nil {
|
||||
dst.V6MasqAddr = ptr.To(*src.V6MasqAddr)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _PeerCloneNeedsRegeneration = Peer(struct {
|
||||
PublicKey key.NodePublic
|
||||
DiscoKey key.DiscoPublic
|
||||
AllowedIPs []netip.Prefix
|
||||
V4MasqAddr *netip.Addr
|
||||
V6MasqAddr *netip.Addr
|
||||
IsJailed bool
|
||||
PersistentKeepalive uint16
|
||||
WGEndpoint key.NodePublic
|
||||
PrivateKey key.NodePrivate
|
||||
Addresses []netip.Prefix
|
||||
}{})
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// ToUAPI writes cfg in UAPI format to w.
|
||||
// Prev is the previous device Config.
|
||||
//
|
||||
// Prev is required so that we can remove now-defunct peers without having to
|
||||
// remove and re-add all peers, and so that we can avoid writing information
|
||||
// about peers that have not changed since the previous time we wrote our
|
||||
// Config.
|
||||
func (cfg *Config) ToUAPI(logf logger.Logf, w io.Writer, prev *Config) error {
|
||||
var stickyErr error
|
||||
set := func(key, value string) {
|
||||
if stickyErr != nil {
|
||||
return
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s=%s\n", key, value)
|
||||
if err != nil {
|
||||
stickyErr = err
|
||||
}
|
||||
}
|
||||
setUint16 := func(key string, value uint16) {
|
||||
set(key, strconv.FormatUint(uint64(value), 10))
|
||||
}
|
||||
setPeer := func(peer Peer) {
|
||||
set("public_key", peer.PublicKey.UntypedHexString())
|
||||
}
|
||||
|
||||
// Device config.
|
||||
if !prev.PrivateKey.Equal(cfg.PrivateKey) {
|
||||
set("private_key", cfg.PrivateKey.UntypedHexString())
|
||||
}
|
||||
|
||||
old := make(map[key.NodePublic]Peer)
|
||||
for _, p := range prev.Peers {
|
||||
old[p.PublicKey] = p
|
||||
}
|
||||
|
||||
// Add/configure all new peers.
|
||||
for _, p := range cfg.Peers {
|
||||
oldPeer, wasPresent := old[p.PublicKey]
|
||||
|
||||
// We only want to write the peer header/version if we're about
|
||||
// to change something about that peer, or if it's a new peer.
|
||||
// Figure out up-front whether we'll need to do anything for
|
||||
// this peer, and skip doing anything if not.
|
||||
//
|
||||
// If the peer was not present in the previous config, this
|
||||
// implies that this is a new peer; set all of these to 'true'
|
||||
// to ensure that we're writing the full peer configuration.
|
||||
willSetEndpoint := oldPeer.WGEndpoint != p.PublicKey || !wasPresent
|
||||
willChangeIPs := !cidrsEqual(oldPeer.AllowedIPs, p.AllowedIPs) || !wasPresent
|
||||
willChangeKeepalive := oldPeer.PersistentKeepalive != p.PersistentKeepalive // if not wasPresent, no need to redundantly set zero (default)
|
||||
|
||||
if !willSetEndpoint && !willChangeIPs && !willChangeKeepalive {
|
||||
// It's safe to skip doing anything here; wireguard-go
|
||||
// will not remove a peer if it's unspecified unless we
|
||||
// tell it to (which we do below if necessary).
|
||||
continue
|
||||
}
|
||||
|
||||
setPeer(p)
|
||||
set("protocol_version", "1")
|
||||
|
||||
// Avoid setting endpoints if the correct one is already known
|
||||
// to WireGuard, because doing so generates a bit more work in
|
||||
// calling magicsock's ParseEndpoint for effectively a no-op.
|
||||
if willSetEndpoint {
|
||||
if wasPresent {
|
||||
// We had an endpoint, and it was wrong.
|
||||
// By construction, this should not happen.
|
||||
// If it does, keep going so that we can recover from it,
|
||||
// but log so that we know about it,
|
||||
// because it is an indicator of other failed invariants.
|
||||
// See corp issue 3016.
|
||||
logf("[unexpected] endpoint changed from %s to %s", oldPeer.WGEndpoint, p.PublicKey)
|
||||
}
|
||||
set("endpoint", p.PublicKey.UntypedHexString())
|
||||
}
|
||||
|
||||
// TODO: replace_allowed_ips is expensive.
|
||||
// If p.AllowedIPs is a strict superset of oldPeer.AllowedIPs,
|
||||
// then skip replace_allowed_ips and instead add only
|
||||
// the new ipps with allowed_ip.
|
||||
if willChangeIPs {
|
||||
set("replace_allowed_ips", "true")
|
||||
for _, ipp := range p.AllowedIPs {
|
||||
set("allowed_ip", ipp.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Set PersistentKeepalive after the peer is otherwise configured,
|
||||
// because it can trigger handshake packets.
|
||||
if willChangeKeepalive {
|
||||
setUint16("persistent_keepalive_interval", p.PersistentKeepalive)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove peers that were present but should no longer be.
|
||||
for _, p := range cfg.Peers {
|
||||
delete(old, p.PublicKey)
|
||||
}
|
||||
for _, p := range old {
|
||||
setPeer(p)
|
||||
set("remove", "true")
|
||||
}
|
||||
|
||||
if stickyErr != nil {
|
||||
stickyErr = fmt.Errorf("ToUAPI: %w", stickyErr)
|
||||
}
|
||||
return stickyErr
|
||||
}
|
||||
|
||||
func cidrsEqual(x, y []netip.Prefix) bool {
|
||||
// TODO: re-implement using netaddr.IPSet.Equal.
|
||||
if len(x) != len(y) {
|
||||
return false
|
||||
}
|
||||
// First see if they're equal in order, without allocating.
|
||||
exact := true
|
||||
for i := range x {
|
||||
if x[i] != y[i] {
|
||||
exact = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if exact {
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, see if they're the same, but out of order.
|
||||
m := make(map[netip.Prefix]bool)
|
||||
for _, v := range x {
|
||||
m[v] = true
|
||||
}
|
||||
for _, v := range y {
|
||||
if !m[v] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+133
-10
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package wgengine provides the Tailscale WireGuard engine interface.
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/gaissmai/bart"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/routemanager"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -41,6 +43,28 @@ type StatusCallback func(*Status, error)
|
||||
// into network map updates.
|
||||
type NetworkMapCallback func(*netmap.NetworkMap)
|
||||
|
||||
// PeerWireGuardState is the current WireGuard session state for a peer.
|
||||
type PeerWireGuardState uint8
|
||||
|
||||
const (
|
||||
// PeerWireGuardStateNone means there is no handshake in progress and no
|
||||
// session key material retained for this peer.
|
||||
PeerWireGuardStateNone PeerWireGuardState = 0
|
||||
|
||||
// PeerWireGuardStateHandshake means a handshake is in progress for this
|
||||
// peer, but there is not currently a usable WireGuard session.
|
||||
PeerWireGuardStateHandshake PeerWireGuardState = 1
|
||||
|
||||
// PeerWireGuardStateEstablished means the peer has a completed WireGuard
|
||||
// session with usable session key material.
|
||||
PeerWireGuardStateEstablished PeerWireGuardState = 2
|
||||
|
||||
// PeerWireGuardStateExpired means the peer's session key material is no
|
||||
// longer considered usable, but final key cleanup or lazy peer removal may
|
||||
// not have happened yet.
|
||||
PeerWireGuardStateExpired PeerWireGuardState = 3
|
||||
)
|
||||
|
||||
// ErrNoChanges is returned by Engine.Reconfig if no changes were made.
|
||||
var ErrNoChanges = errors.New("no changes made to Engine config")
|
||||
|
||||
@@ -76,9 +100,19 @@ type Engine interface {
|
||||
// Unlike Reconfig, it does not return ErrNoChanges.
|
||||
ResetAndStop() (*Status, error)
|
||||
|
||||
// PeerForIP returns the node to which the provided IP routes,
|
||||
// if any. If none is found, (nil, false) is returned.
|
||||
PeerForIP(netip.Addr) (_ PeerForIP, ok bool)
|
||||
// SetPeerForIPFunc installs the IP-to-node lookup used by the
|
||||
// engine's internal cold paths (Ping, TSMP, pendopen diagnostics).
|
||||
// It parallels [Engine.SetPeerByIPPacketFunc] but returns richer
|
||||
// data (a full NodeView, the matched route prefix, and the IsSelf
|
||||
// flag).
|
||||
//
|
||||
// If fn is nil, those lookups fail for every IP.
|
||||
//
|
||||
// LocalBackend installs a func backed by the live nodeBackend for
|
||||
// exact-match and self addresses, with the RouteManager's outbound
|
||||
// table supplying the subnet-route / exit-node fallback; the engine
|
||||
// itself holds no peer-lookup state on this path.
|
||||
SetPeerForIPFunc(fn func(netip.Addr) (_ PeerForIP, ok bool))
|
||||
|
||||
// GetFilter returns the current packet filter, if any.
|
||||
GetFilter() *filter.Filter
|
||||
@@ -93,6 +127,17 @@ type Engine interface {
|
||||
// SetJailedFilter updates the packet filter for jailed nodes.
|
||||
SetJailedFilter(*filter.Filter)
|
||||
|
||||
// SetPeerRoutes updates the per-peer route attributes used by the
|
||||
// tun-layer data plane for per-packet NAT rewrites and
|
||||
// jailed-filter selection. native4 and native6 are this node's own
|
||||
// Tailscale addresses, and routes maps each peer's addresses and
|
||||
// routed prefixes to its attributes; it is a shared immutable
|
||||
// snapshot from [routemanager.RouteManager.Outbound].
|
||||
//
|
||||
// A nil routes table disables all per-packet peer processing;
|
||||
// callers pass nil when no current peer has any such attributes.
|
||||
SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute])
|
||||
|
||||
// SetStatusCallback sets the function to call when the
|
||||
// WireGuard status changes.
|
||||
SetStatusCallback(StatusCallback)
|
||||
@@ -116,12 +161,9 @@ type Engine interface {
|
||||
// You don't have to call this.
|
||||
Done() <-chan struct{}
|
||||
|
||||
// SetNetworkMap informs the engine of the latest network map
|
||||
// from the server. The network map's DERPMap field should be
|
||||
// ignored as as it might be disabled; get it from SetDERPMap
|
||||
// instead.
|
||||
// The network map should only be read from.
|
||||
SetNetworkMap(*netmap.NetworkMap)
|
||||
// SetSelfNode informs the engine of the current self node.
|
||||
// The zero (invalid) NodeView indicates no self node.
|
||||
SetSelfNode(tailcfg.NodeView)
|
||||
|
||||
// UpdateStatus populates the network state using the provided
|
||||
// status builder.
|
||||
@@ -137,4 +179,85 @@ type Engine interface {
|
||||
// packets traversing the data path. The hook can be uninstalled by
|
||||
// calling this function with a nil value.
|
||||
InstallCaptureHook(packet.CaptureCallback)
|
||||
|
||||
// SetPeerByIPPacketFunc installs a callback used by wireguard-go to
|
||||
// look up which peer should handle an outbound packet by destination IP.
|
||||
SetPeerByIPPacketFunc(func(netip.Addr) (_ key.NodePublic, ok bool))
|
||||
|
||||
// SetPeerConfigFunc installs the live source of per-peer WireGuard
|
||||
// configuration: given a peer's public key, fn returns its current
|
||||
// WireGuard configuration, including the prefixes it may originate and
|
||||
// its optional pre-shared key. It returns ok=false if the peer is unknown,
|
||||
// in which case it must not exist in the WireGuard device. The engine installs a single
|
||||
// [device.PeerLookupFunc] wrapping fn, so lazily-created peers
|
||||
// always see current state and the lookup func never needs to be
|
||||
// reinstalled as peers come and go.
|
||||
//
|
||||
// It is expected to be called once during LocalBackend construction,
|
||||
// before the first [Engine.Reconfig]. fn is called rarely (when
|
||||
// wireguard-go first hears from a peer it doesn't have) and may
|
||||
// acquire locks.
|
||||
SetPeerConfigFunc(fn func(key.NodePublic) (config wgcfg.PeerConfig, ok bool))
|
||||
|
||||
// SyncDevicePeer synchronizes the WireGuard device's state for a
|
||||
// single peer with the config source installed via
|
||||
// [Engine.SetPeerConfigFunc]: if the source no longer knows the
|
||||
// peer, it is removed from the device; if the peer is active in the
|
||||
// device, its allowed IPs and pre-shared key are updated. It does O(1)
|
||||
// work (plus the config source lookup) and is intended to be called for
|
||||
// each peer
|
||||
// added, updated, or removed by an incremental netmap delta,
|
||||
// avoiding a full [Engine.Reconfig].
|
||||
//
|
||||
// It is a no-op if no config source is installed.
|
||||
SyncDevicePeer(key.NodePublic)
|
||||
|
||||
// MarkDevicePeerForHandshake marks the peer for an opportunistic handshake in the
|
||||
// WireGuard device.
|
||||
//
|
||||
// LocalBackend calls it when a peer's disco key changes, which
|
||||
// means the peer restarted and its old sessions are dead, or that control
|
||||
// caught up to a key learned via TSMP previously.
|
||||
MarkDevicePeerForHandshake(key.NodePublic)
|
||||
|
||||
// SetNetLogSource installs the [NetLogSource] consulted by the
|
||||
// engine's network flow logger for node lookups and the current
|
||||
// audit logging identity.
|
||||
//
|
||||
// It is expected to be called once during LocalBackend construction,
|
||||
// before any [Engine.Reconfig] call that starts up the network logger.
|
||||
SetNetLogSource(NetLogSource)
|
||||
|
||||
// SetWGPeerLookup installs the function used by the engine's
|
||||
// wireguard-go log wrapper to rewrite peer references in log lines
|
||||
// (mapping wireguard-go's "peer(XXXX…YYYY)" form to the
|
||||
// Tailscale-conventional short string form).
|
||||
//
|
||||
// It is expected to be called once during LocalBackend construction.
|
||||
// The function is called concurrently and must be safe to call with
|
||||
// no Engine locks held.
|
||||
SetWGPeerLookup(func(wgString string) (tsString string, ok bool))
|
||||
|
||||
// SetPeerSessionStateFunc installs a callback used to observe WireGuard
|
||||
// peer session state transitions.
|
||||
//
|
||||
// Calls are serialized per Engine and delivered in transition order from
|
||||
// wireguard-go, while wireguard-go is holding locks. The callback must be
|
||||
// cheap and must not call back into wireguard-go.
|
||||
//
|
||||
// It does not replay current state. Callers that need a complete view should
|
||||
// set it before peers are started or lazily created, and maintain any
|
||||
// snapshots, sequence numbers, and pubsub state outside wireguard-go.
|
||||
//
|
||||
// In Tailscale, the usual implementation is
|
||||
// ipnlocal.LocalBackend.onPeerWireGuardState, installed early in
|
||||
// LocalBackend construction.
|
||||
SetPeerSessionStateFunc(func(key.NodePublic, PeerWireGuardState))
|
||||
|
||||
SetPeerPriorityMessageOnEstablishmentFunc(fn func(key.NodePublic) (msg []byte))
|
||||
|
||||
// ProbeLocks acquires and releases the engine's internal locks so
|
||||
// that [ipnlocal.LocalBackend]'s watchdog can detect deadlocks in
|
||||
// the engine. It is otherwise a no-op.
|
||||
ProbeLocks()
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package wgint provides somewhat shady access to wireguard-go
|
||||
|
||||
+84
-55
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package wglog contains logging helpers for wireguard-go.
|
||||
@@ -11,33 +11,42 @@ import (
|
||||
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// A Logger is a wireguard-go log wrapper that cleans up and rewrites log lines.
|
||||
// It can be modified at run time to adjust to new wireguard-go configurations.
|
||||
// It rewrites wireguard-go peer references like "peer(XXXX…YYYY)" into the
|
||||
// Tailscale-conventional short string form like "[XXXXX]".
|
||||
type Logger struct {
|
||||
DeviceLogger *device.Logger
|
||||
replace syncs.AtomicValue[map[string]string]
|
||||
mu sync.Mutex // protects strs
|
||||
strs map[key.NodePublic]*strCache // cached strs used to populate replace
|
||||
}
|
||||
|
||||
// strCache holds a wireguard-go and a Tailscale style peer string.
|
||||
type strCache struct {
|
||||
wg, ts string
|
||||
used bool // track whether this strCache was used in a particular round
|
||||
// lookup, when non-nil, returns the Tailscale-conventional short
|
||||
// string for a wireguard-go-formatted peer string (e.g. it maps
|
||||
// "peer(IMTB…r7lM)" to "[IMTBr]"), or "", false if no peer matches.
|
||||
// It is set once at construction and not changed afterwards.
|
||||
lookup func(wgString string) (tsString string, ok bool)
|
||||
|
||||
mu sync.Mutex // protects cache
|
||||
|
||||
// cache memoizes lookup results. The key is the wireguard-go
|
||||
// peer-string form ("peer(XXXX…YYYY)") and the value is the
|
||||
// Tailscale-conventional short-string form ("[XXXXX]").
|
||||
// It is cleared in bulk by Invalidate when the underlying peer set
|
||||
// may have changed.
|
||||
cache map[string]string
|
||||
}
|
||||
|
||||
// NewLogger creates a new logger for use with wireguard-go.
|
||||
// This logger silences repetitive/unhelpful noisy log lines
|
||||
// and rewrites peer keys from wireguard-go into Tailscale format.
|
||||
func NewLogger(logf logger.Logf) *Logger {
|
||||
//
|
||||
// lookup, if non-nil, is consulted on cache misses to translate
|
||||
// wireguard-go peer references in log lines. If lookup is nil,
|
||||
// peer references are passed through unchanged.
|
||||
func NewLogger(logf logger.Logf, lookup func(wgString string) (tsString string, ok bool)) *Logger {
|
||||
const prefix = "wg: "
|
||||
ret := new(Logger)
|
||||
ret := &Logger{lookup: lookup}
|
||||
wrapper := func(format string, args ...any) {
|
||||
if strings.Contains(format, "Routine:") && !strings.Contains(format, "receive incoming") {
|
||||
// wireguard-go logs as it starts and stops routines.
|
||||
@@ -60,32 +69,37 @@ func NewLogger(logf logger.Logf) *Logger {
|
||||
// useful.
|
||||
return
|
||||
}
|
||||
replace := ret.replace.Load()
|
||||
if replace == nil {
|
||||
// No replacements specified; log as originally planned.
|
||||
if ret.lookup == nil {
|
||||
// No lookup; log as originally planned.
|
||||
logf(format, args...)
|
||||
return
|
||||
}
|
||||
// Duplicate the args slice so that we can modify it.
|
||||
// This is not always required, but the code required to avoid it is not worth the complexity.
|
||||
newargs := make([]any, len(args))
|
||||
copy(newargs, args)
|
||||
for i, arg := range newargs {
|
||||
// We want to replace *device.Peer args with the Tailscale-formatted version of themselves.
|
||||
// Using *device.Peer directly makes this hard to test, so we string any fmt.Stringers,
|
||||
// and if the string ends up looking exactly like a known Peer, we replace it.
|
||||
// This is slightly imprecise, in that we don't check the formatting verb. Oh well.
|
||||
// Replace any *device.Peer-shaped fmt.Stringer args with the
|
||||
// Tailscale-formatted version of themselves. Using *device.Peer
|
||||
// directly makes this hard to test, so we string any fmt.Stringers,
|
||||
// and if the string ends up matching a known peer, we substitute.
|
||||
// This is slightly imprecise, in that we don't check the formatting
|
||||
// verb. Oh well.
|
||||
var newargs []any
|
||||
for i, arg := range args {
|
||||
s, ok := arg.(fmt.Stringer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
wgStr := s.String()
|
||||
tsStr, ok := replace[wgStr]
|
||||
tsStr, ok := ret.peerStringFor(s.String())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if newargs == nil {
|
||||
newargs = make([]any, len(args))
|
||||
copy(newargs, args)
|
||||
}
|
||||
newargs[i] = tsStr
|
||||
}
|
||||
if newargs == nil {
|
||||
logf(format, args...)
|
||||
return
|
||||
}
|
||||
logf(format, newargs...)
|
||||
}
|
||||
if envknob.Bool("TS_DEBUG_RAW_WGLOG") {
|
||||
@@ -95,36 +109,51 @@ func NewLogger(logf logger.Logf) *Logger {
|
||||
Verbosef: logger.WithPrefix(wrapper, prefix+"[v2] "),
|
||||
Errorf: logger.WithPrefix(wrapper, prefix),
|
||||
}
|
||||
ret.strs = make(map[key.NodePublic]*strCache)
|
||||
return ret
|
||||
}
|
||||
|
||||
// SetPeers adjusts x to rewrite the peer public keys found in peers.
|
||||
// SetPeers is safe for concurrent use.
|
||||
func (x *Logger) SetPeers(peers []wgcfg.Peer) {
|
||||
// peerStringFor returns the Tailscale-conventional short string for
|
||||
// wgString, if wgString is a wireguard-go-formatted peer string for a
|
||||
// peer known to x.lookup. Results are memoized in x.cache for
|
||||
// subsequent log lines.
|
||||
func (x *Logger) peerStringFor(wgString string) (tsString string, ok bool) {
|
||||
// Fast path: only strings shaped like wireguard-go's
|
||||
// "peer(XXXX…YYYY)" output can be peer references; skip the cache
|
||||
// and lookup call for anything else (e.g. arbitrary stringers used
|
||||
// in unrelated log args).
|
||||
if !strings.HasPrefix(wgString, "peer(") || !strings.HasSuffix(wgString, ")") {
|
||||
return "", false
|
||||
}
|
||||
x.mu.Lock()
|
||||
defer x.mu.Unlock()
|
||||
// Construct a new peer public key log rewriter.
|
||||
replace := make(map[string]string)
|
||||
for _, peer := range peers {
|
||||
c, ok := x.strs[peer.PublicKey] // look up cached strs
|
||||
if !ok {
|
||||
wg := peer.PublicKey.WireGuardGoString()
|
||||
ts := peer.PublicKey.ShortString()
|
||||
c = &strCache{wg: wg, ts: ts}
|
||||
x.strs[peer.PublicKey] = c
|
||||
}
|
||||
c.used = true
|
||||
replace[c.wg] = c.ts
|
||||
if v, ok := x.cache[wgString]; ok {
|
||||
x.mu.Unlock()
|
||||
return v, true
|
||||
}
|
||||
// Remove any unused cached strs.
|
||||
for k, c := range x.strs {
|
||||
if !c.used {
|
||||
delete(x.strs, k)
|
||||
continue
|
||||
}
|
||||
// Mark c as unused for next round.
|
||||
c.used = false
|
||||
x.mu.Unlock()
|
||||
|
||||
// Call lookup without x.mu held so we don't have to reason about
|
||||
// deadlocks between x.mu and any locks lookup acquires.
|
||||
// In the worst case, two goroutines concurrently miss the cache
|
||||
// for the same wgString and both call lookup; the redundant call
|
||||
// is wasted but harmless.
|
||||
tsString, ok = x.lookup(wgString)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
x.replace.Store(replace)
|
||||
x.mu.Lock()
|
||||
mak.Set(&x.cache, wgString, tsString)
|
||||
x.mu.Unlock()
|
||||
return tsString, true
|
||||
}
|
||||
|
||||
// Invalidate clears the peer-string rewrite cache.
|
||||
//
|
||||
// Callers should invoke Invalidate when the set of wireguard-go peers
|
||||
// has changed so that the next log line re-resolves peer references
|
||||
// against the current lookup. It is safe to call concurrently with
|
||||
// logging.
|
||||
func (x *Logger) Invalidate() {
|
||||
x.mu.Lock()
|
||||
clear(x.cache)
|
||||
x.mu.Unlock()
|
||||
}
|
||||
Reference in new issue
Block a user