feature/portlist: pull portlist service porting into extension, use eventbus

And yay: tsnet (and thus k8s-operator etc) no longer depends on
portlist! And LocalBackend is smaller.

Removes 50 KB from the minimal binary.

Updates #12614

Change-Id: Iee04057053dc39305303e8bd1d9599db8368d926
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-24 15:12:24 -07:00
committed by Brad Fitzpatrick
parent bbc5107d7d
commit 45d635cc98
18 changed files with 277 additions and 111 deletions

View File

@@ -813,7 +813,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+ tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+
tailscale.com/ipn/ipnstate from tailscale.com/client/local+ tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/ipn/localapi from tailscale.com/tsnet tailscale.com/ipn/localapi from tailscale.com/tsnet
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
tailscale.com/ipn/store/kubestore from tailscale.com/cmd/k8s-operator+ tailscale.com/ipn/store/kubestore from tailscale.com/cmd/k8s-operator+
@@ -861,7 +860,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/net/netknob from tailscale.com/logpolicy+ tailscale.com/net/netknob from tailscale.com/logpolicy+
💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+ 💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
tailscale.com/net/netutil from tailscale.com/client/local+ tailscale.com/net/netutil from tailscale.com/client/local+
tailscale.com/net/netx from tailscale.com/control/controlclient+ tailscale.com/net/netx from tailscale.com/control/controlclient+
tailscale.com/net/packet from tailscale.com/net/connstats+ tailscale.com/net/packet from tailscale.com/net/connstats+
@@ -885,7 +883,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/net/udprelay/status from tailscale.com/client/local tailscale.com/net/udprelay/status from tailscale.com/client/local
tailscale.com/omit from tailscale.com/ipn/conffile tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+ tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/posture from tailscale.com/ipn/ipnlocal tailscale.com/posture from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+ tailscale.com/proxymap from tailscale.com/tsd+
💣 tailscale.com/safesocket from tailscale.com/client/local+ 💣 tailscale.com/safesocket from tailscale.com/client/local+
@@ -931,7 +928,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/util/cmpver from tailscale.com/clientupdate+ tailscale.com/util/cmpver from tailscale.com/clientupdate+
tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+ tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+ L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/appc+ tailscale.com/util/dnsname from tailscale.com/appc+
tailscale.com/util/eventbus from tailscale.com/tsd+ tailscale.com/util/eventbus from tailscale.com/tsd+
tailscale.com/util/execqueue from tailscale.com/appc+ tailscale.com/util/execqueue from tailscale.com/appc+

View File

@@ -278,6 +278,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/feature/debugportmapper from tailscale.com/feature/condregister tailscale.com/feature/debugportmapper from tailscale.com/feature/condregister
tailscale.com/feature/drive from tailscale.com/feature/condregister tailscale.com/feature/drive from tailscale.com/feature/condregister
L tailscale.com/feature/linuxdnsfight from tailscale.com/feature/condregister L tailscale.com/feature/linuxdnsfight from tailscale.com/feature/condregister
tailscale.com/feature/portlist from tailscale.com/feature/condregister
tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper tailscale.com/feature/portmapper from tailscale.com/feature/condregister/portmapper
tailscale.com/feature/relayserver from tailscale.com/feature/condregister tailscale.com/feature/relayserver from tailscale.com/feature/condregister
tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+ tailscale.com/feature/syspolicy from tailscale.com/feature/condregister+
@@ -299,7 +300,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/client/local+ tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver+ tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal tailscale.com/ipn/policy from tailscale.com/feature/portlist
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+ tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
@@ -360,7 +361,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/udprelay/status from tailscale.com/client/local+ tailscale.com/net/udprelay/status from tailscale.com/client/local+
tailscale.com/omit from tailscale.com/ipn/conffile tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+ tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal 💣 tailscale.com/portlist from tailscale.com/feature/portlist
tailscale.com/posture from tailscale.com/ipn/ipnlocal tailscale.com/posture from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+ tailscale.com/proxymap from tailscale.com/tsd+
💣 tailscale.com/safesocket from tailscale.com/client/local+ 💣 tailscale.com/safesocket from tailscale.com/client/local+

View File

@@ -185,3 +185,16 @@ func TestOmitDBus(t *testing.T) {
}, },
}.Check(t) }.Check(t)
} }
func TestOmitPortlist(t *testing.T) {
deptest.DepChecker{
GOOS: "linux",
GOARCH: "amd64",
Tags: "ts_omit_portlist,ts_include_cli",
OnDep: func(dep string) {
if strings.Contains(dep, "portlist") {
t.Errorf("unexpected dep: %q", dep)
}
},
}.Check(t)
}

View File

@@ -255,7 +255,6 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+ tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+
tailscale.com/ipn/ipnstate from tailscale.com/client/local+ tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/ipn/localapi from tailscale.com/tsnet tailscale.com/ipn/localapi from tailscale.com/tsnet
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
@@ -292,7 +291,6 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
tailscale.com/net/netknob from tailscale.com/logpolicy+ tailscale.com/net/netknob from tailscale.com/logpolicy+
💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+ 💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
tailscale.com/net/netutil from tailscale.com/client/local+ tailscale.com/net/netutil from tailscale.com/client/local+
tailscale.com/net/netx from tailscale.com/control/controlclient+ tailscale.com/net/netx from tailscale.com/control/controlclient+
tailscale.com/net/packet from tailscale.com/ipn/ipnlocal+ tailscale.com/net/packet from tailscale.com/ipn/ipnlocal+
@@ -316,7 +314,6 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
tailscale.com/net/udprelay/status from tailscale.com/client/local tailscale.com/net/udprelay/status from tailscale.com/client/local
tailscale.com/omit from tailscale.com/ipn/conffile tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+ tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/posture from tailscale.com/ipn/ipnlocal tailscale.com/posture from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+ tailscale.com/proxymap from tailscale.com/tsd+
💣 tailscale.com/safesocket from tailscale.com/client/local+ 💣 tailscale.com/safesocket from tailscale.com/client/local+
@@ -361,7 +358,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
tailscale.com/util/cmpver from tailscale.com/clientupdate+ tailscale.com/util/cmpver from tailscale.com/clientupdate+
tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+ tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+ L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/appc+ tailscale.com/util/dnsname from tailscale.com/appc+
tailscale.com/util/eventbus from tailscale.com/ipn/localapi+ tailscale.com/util/eventbus from tailscale.com/ipn/localapi+
tailscale.com/util/execqueue from tailscale.com/appc+ tailscale.com/util/execqueue from tailscale.com/appc+

View File

@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build ts_omit_portlist
package buildfeatures
// HasPortList is whether the binary was built with support for modular feature "Optionally advertise listening service ports".
// Specifically, it's whether the binary was NOT built with the "ts_omit_portlist" build tag.
// It's a const so it can be used for dead code elimination.
const HasPortList = false

View File

@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build !ts_omit_portlist
package buildfeatures
// HasPortList is whether the binary was built with support for modular feature "Optionally advertise listening service ports".
// Specifically, it's whether the binary was NOT built with the "ts_omit_portlist" build tag.
// It's a const so it can be used for dead code elimination.
const HasPortList = true

View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_portlist
package condregister
import _ "tailscale.com/feature/portlist"

View File

@@ -114,6 +114,7 @@ var Features = map[FeatureTag]FeatureMeta{
Desc: "Outbound localhost HTTP/SOCK5 proxy support", Desc: "Outbound localhost HTTP/SOCK5 proxy support",
Deps: []FeatureTag{"netstack"}, Deps: []FeatureTag{"netstack"},
}, },
"portlist": {"PortList", "Optionally advertise listening service ports", nil},
"portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support", nil}, "portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support", nil},
"netstack": {"Netstack", "gVisor netstack (userspace networking) support (TODO; not yet omittable)", nil}, "netstack": {"Netstack", "gVisor netstack (userspace networking) support (TODO; not yet omittable)", nil},
"networkmanager": { "networkmanager": {

View File

@@ -0,0 +1,157 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package portlist contains code to poll the local system for open ports
// and report them to the control plane, if enabled on the tailnet.
package portlist
import (
"context"
"sync/atomic"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/policy"
"tailscale.com/portlist"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
"tailscale.com/version"
)
func init() {
ipnext.RegisterExtension("portlist", newExtension)
}
func newExtension(logf logger.Logf, sb ipnext.SafeBackend) (ipnext.Extension, error) {
busClient := sb.Sys().Bus.Get().Client("portlist")
e := &Extension{
sb: sb,
busClient: busClient,
logf: logger.WithPrefix(logf, "portlist: "),
pub: eventbus.Publish[ipnlocal.PortlistServices](busClient),
pollerDone: make(chan struct{}),
wakePoller: make(chan struct{}),
}
e.ctx, e.ctxCancel = context.WithCancel(context.Background())
return e, nil
}
// Extension implements the portlist extension.
type Extension struct {
ctx context.Context
ctxCancel context.CancelFunc
pollerDone chan struct{} // close-only chan when poller goroutine exits
wakePoller chan struct{} // best effort chan to wake poller from sleep
busClient *eventbus.Client
pub *eventbus.Publisher[ipnlocal.PortlistServices]
logf logger.Logf
sb ipnext.SafeBackend
host ipnext.Host // from Init
shieldsUp atomic.Bool
shouldUploadServicesAtomic atomic.Bool
}
func (e *Extension) Name() string { return "portlist" }
func (e *Extension) Shutdown() error {
e.ctxCancel()
e.busClient.Close()
<-e.pollerDone
return nil
}
func (e *Extension) Init(h ipnext.Host) error {
if !envknob.BoolDefaultTrue("TS_PORTLIST") {
return ipnext.SkipExtension
}
e.host = h
h.Hooks().ShouldUploadServices.Set(e.shouldUploadServicesAtomic.Load)
h.Hooks().ProfileStateChange.Add(e.onChangeProfile)
h.Hooks().OnSelfChange.Add(e.onSelfChange)
// TODO(nickkhyl): remove this after the profileManager refactoring.
// See tailscale/tailscale#15974.
// This same workaround appears in feature/taildrop/ext.go.
profile, prefs := h.Profiles().CurrentProfileState()
e.onChangeProfile(profile, prefs, false)
go e.runPollLoop()
return nil
}
func (e *Extension) onSelfChange(tailcfg.NodeView) {
e.updateShouldUploadServices()
}
func (e *Extension) onChangeProfile(_ ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
e.shieldsUp.Store(prefs.ShieldsUp())
e.updateShouldUploadServices()
}
func (e *Extension) updateShouldUploadServices() {
v := !e.shieldsUp.Load() && e.host.NodeBackend().CollectServices()
if e.shouldUploadServicesAtomic.CompareAndSwap(!v, v) && v {
// Upon transition from false to true (enabling service reporting), try
// to wake the poller to do an immediate poll if it's sleeping.
// It's not a big deal if we miss waking it. It'll get to it soon enough.
select {
case e.wakePoller <- struct{}{}:
default:
}
}
}
// runPollLoop is a goroutine that periodically checks the open
// ports and publishes them if they've changed.
func (e *Extension) runPollLoop() {
defer close(e.pollerDone)
var poller portlist.Poller
ticker, tickerChannel := e.sb.Clock().NewTicker(portlist.PollInterval())
defer ticker.Stop()
for {
select {
case <-tickerChannel:
case <-e.wakePoller:
case <-e.ctx.Done():
return
}
if !e.shouldUploadServicesAtomic.Load() {
continue
}
ports, changed, err := poller.Poll()
if err != nil {
e.logf("Poll: %v", err)
// TODO: this is kinda weird that we just return here and never try
// again. Maybe that was because all errors are assumed to be
// permission errors and thus permanent? Audit varioys OS
// implementation and check error types, and then make this check
// for permanent vs temporary errors and keep looping with a backoff
// for temporary errors? But for now we just give up, like we always
// have.
return
}
if !changed {
continue
}
sl := []tailcfg.Service{}
for _, p := range ports {
s := tailcfg.Service{
Proto: tailcfg.ServiceProto(p.Proto),
Port: p.Port,
Description: p.Process,
}
if policy.IsInterestingService(s, version.OS()) {
sl = append(sl, s)
}
}
e.pub.Publish(ipnlocal.PortlistServices(sl))
}
}

View File

@@ -105,6 +105,7 @@ func (e *Extension) Init(h ipnext.Host) error {
// TODO(nickkhyl): remove this after the profileManager refactoring. // TODO(nickkhyl): remove this after the profileManager refactoring.
// See tailscale/tailscale#15974. // See tailscale/tailscale#15974.
// This same workaround appears in feature/portlist/portlist.go.
profile, prefs := h.Profiles().CurrentProfileState() profile, prefs := h.Profiles().CurrentProfileState()
e.onChangeProfile(profile, prefs, false) e.onChangeProfile(profile, prefs, false)
return nil return nil

View File

@@ -372,6 +372,10 @@ type Hooks struct {
// SetPeerStatus is called to mutate PeerStatus. // SetPeerStatus is called to mutate PeerStatus.
// Callers must only use NodeBackend to read data. // Callers must only use NodeBackend to read data.
SetPeerStatus feature.Hooks[func(*ipnstate.PeerStatus, tailcfg.NodeView, NodeBackend)] SetPeerStatus feature.Hooks[func(*ipnstate.PeerStatus, tailcfg.NodeView, NodeBackend)]
// ShouldUploadServices reports whether this node should include services
// in Hostinfo from the portlist extension.
ShouldUploadServices feature.Hook[func() bool]
} }
// NodeBackend is an interface to query the current node and its peers. // NodeBackend is an interface to query the current node and its peers.
@@ -398,4 +402,9 @@ type NodeBackend interface {
// It effectively just reports whether PeerAPIBase(node) is non-empty, but // It effectively just reports whether PeerAPIBase(node) is non-empty, but
// potentially more efficiently. // potentially more efficiently.
PeerHasPeerAPI(tailcfg.NodeView) bool PeerHasPeerAPI(tailcfg.NodeView) bool
// CollectServices reports whether the control plane is telling this
// node that the portlist service collection is desirable, should it
// choose to report them.
CollectServices() bool
} }

View File

@@ -61,7 +61,6 @@ import (
"tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnext"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
"tailscale.com/log/sockstatlog" "tailscale.com/log/sockstatlog"
"tailscale.com/logpolicy" "tailscale.com/logpolicy"
"tailscale.com/net/dns" "tailscale.com/net/dns"
@@ -77,7 +76,6 @@ import (
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial" "tailscale.com/net/tsdial"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/portlist"
"tailscale.com/posture" "tailscale.com/posture"
"tailscale.com/syncs" "tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
@@ -211,12 +209,10 @@ type LocalBackend struct {
pushDeviceToken syncs.AtomicValue[string] pushDeviceToken syncs.AtomicValue[string]
backendLogID logid.PublicID backendLogID logid.PublicID
unregisterSysPolicyWatch func() unregisterSysPolicyWatch func()
portpoll *portlist.Poller // may be nil varRoot string // or empty if SetVarRoot never called
portpollOnce sync.Once // guards starting readPoller logFlushFunc func() // or nil if SetLogFlusher wasn't called
varRoot string // or empty if SetVarRoot never called em *expiryManager // non-nil; TODO(nickkhyl): move to nodeBackend
logFlushFunc func() // or nil if SetLogFlusher wasn't called sshAtomicBool atomic.Bool // TODO(nickkhyl): move to nodeBackend
em *expiryManager // non-nil; TODO(nickkhyl): move to nodeBackend
sshAtomicBool atomic.Bool // TODO(nickkhyl): move to nodeBackend
// webClientAtomicBool controls whether the web client is running. This should // webClientAtomicBool controls whether the web client is running. This should
// be true unless the disable-web-client node attribute has been set. // be true unless the disable-web-client node attribute has been set.
webClientAtomicBool atomic.Bool // TODO(nickkhyl): move to nodeBackend webClientAtomicBool atomic.Bool // TODO(nickkhyl): move to nodeBackend
@@ -522,7 +518,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
pm: pm, pm: pm,
backendLogID: logID, backendLogID: logID,
state: ipn.NoState, state: ipn.NoState,
portpoll: new(portlist.Poller),
em: newExpiryManager(logf, sys.Bus.Get()), em: newExpiryManager(logf, sys.Bus.Get()),
loginFlags: loginFlags, loginFlags: loginFlags,
clock: clock, clock: clock,
@@ -619,6 +614,12 @@ func (b *LocalBackend) consumeEventbusTopics(ec *eventbus.Client) func(*eventbus
healthChangeSub := eventbus.Subscribe[health.Change](ec) healthChangeSub := eventbus.Subscribe[health.Change](ec)
changeDeltaSub := eventbus.Subscribe[netmon.ChangeDelta](ec) changeDeltaSub := eventbus.Subscribe[netmon.ChangeDelta](ec)
var portlist <-chan PortlistServices
if buildfeatures.HasPortList {
portlistSub := eventbus.Subscribe[PortlistServices](ec)
portlist = portlistSub.Events()
}
return func(ec *eventbus.Client) { return func(ec *eventbus.Client) {
for { for {
select { select {
@@ -632,6 +633,10 @@ func (b *LocalBackend) consumeEventbusTopics(ec *eventbus.Client) func(*eventbus
b.onHealthChange(change) b.onHealthChange(change)
case changeDelta := <-changeDeltaSub.Events(): case changeDelta := <-changeDeltaSub.Events():
b.linkChange(&changeDelta) b.linkChange(&changeDelta)
case pl := <-portlist:
if buildfeatures.HasPortList { // redundant, but explicit for linker deadcode and humans
b.setPortlistServices(pl)
}
} }
} }
} }
@@ -2300,15 +2305,6 @@ func (b *LocalBackend) SetControlClientGetterForTesting(newControlClient func(co
b.ccGen = newControlClient b.ccGen = newControlClient
} }
// DisablePortPollerForTest disables the port list poller for tests.
// It must be called before Start.
func (b *LocalBackend) DisablePortPollerForTest() {
testenv.AssertInTest()
b.mu.Lock()
defer b.mu.Unlock()
b.portpoll = nil
}
// PeersForTest returns all the current peers, sorted by Node.ID, // PeersForTest returns all the current peers, sorted by Node.ID,
// for integration tests in another repo. // for integration tests in another repo.
func (b *LocalBackend) PeersForTest() []tailcfg.NodeView { func (b *LocalBackend) PeersForTest() []tailcfg.NodeView {
@@ -2457,12 +2453,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
persistv = new(persist.Persist) persistv = new(persist.Persist)
} }
if b.portpoll != nil {
b.portpollOnce.Do(func() {
b.goTracker.Go(b.readPoller)
})
}
discoPublic := b.MagicConn().DiscoPublicKey() discoPublic := b.MagicConn().DiscoPublicKey()
var err error var err error
@@ -2906,57 +2896,6 @@ func shrinkDefaultRoute(route netip.Prefix, localInterfaceRoutes *netipx.IPSet,
return b.IPSet() return b.IPSet()
} }
// readPoller is a goroutine that receives service lists from
// b.portpoll and propagates them into the controlclient's HostInfo.
func (b *LocalBackend) readPoller() {
if !envknob.BoolDefaultTrue("TS_PORTLIST") {
return
}
ticker, tickerChannel := b.clock.NewTicker(portlist.PollInterval())
defer ticker.Stop()
for {
select {
case <-tickerChannel:
case <-b.ctx.Done():
return
}
if !b.shouldUploadServices() {
continue
}
ports, changed, err := b.portpoll.Poll()
if err != nil {
b.logf("error polling for open ports: %v", err)
return
}
if !changed {
continue
}
sl := []tailcfg.Service{}
for _, p := range ports {
s := tailcfg.Service{
Proto: tailcfg.ServiceProto(p.Proto),
Port: p.Port,
Description: p.Process,
}
if policy.IsInterestingService(s, version.OS()) {
sl = append(sl, s)
}
}
b.mu.Lock()
if b.hostinfo == nil {
b.hostinfo = new(tailcfg.Hostinfo)
}
b.hostinfo.Services = sl
b.mu.Unlock()
b.doSetHostinfoFilterServices()
}
}
// GetPushDeviceToken returns the push notification device token. // GetPushDeviceToken returns the push notification device token.
func (b *LocalBackend) GetPushDeviceToken() string { func (b *LocalBackend) GetPushDeviceToken() string {
return b.pushDeviceToken.Load() return b.pushDeviceToken.Load()
@@ -3853,23 +3792,6 @@ func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineSt
return ret return ret
} }
// shouldUploadServices reports whether this node should include services
// in Hostinfo. When the user preferences currently request "shields up"
// mode, all inbound connections are refused, so services are not reported.
// Otherwise, shouldUploadServices respects NetMap.CollectServices.
// TODO(nickkhyl): move this into [nodeBackend]?
func (b *LocalBackend) shouldUploadServices() bool {
b.mu.Lock()
defer b.mu.Unlock()
p := b.pm.CurrentPrefs()
nm := b.currentNode().NetMap()
if !p.Valid() || nm == nil {
return false // default to safest setting
}
return !p.ShieldsUp() && nm.CollectServices
}
// SetCurrentUser is used to implement support for multi-user systems (only // SetCurrentUser is used to implement support for multi-user systems (only
// Windows 2022-11-25). On such systems, the actor is used to determine which // Windows 2022-11-25). On such systems, the actor is used to determine which
// user's state should be used. The current user is maintained by active // user's state should be used. The current user is maintained by active
@@ -4812,6 +4734,25 @@ func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
return ret return ret
} }
// PortlistServices is an eventbus topic for the portlist extension
// to advertise the running services on the host.
type PortlistServices []tailcfg.Service
func (b *LocalBackend) setPortlistServices(sl []tailcfg.Service) {
if !buildfeatures.HasPortList { // redundant, but explicit for linker deadcode and humans
return
}
b.mu.Lock()
if b.hostinfo == nil {
b.hostinfo = new(tailcfg.Hostinfo)
}
b.hostinfo.Services = sl
b.mu.Unlock()
b.doSetHostinfoFilterServices()
}
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient, // doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
// possibly after mangling the given hostinfo. // possibly after mangling the given hostinfo.
// //
@@ -4837,13 +4778,15 @@ func (b *LocalBackend) doSetHostinfoFilterServices() {
// TODO(maisem,bradfitz): store hostinfo as a view, not as a mutable struct. // TODO(maisem,bradfitz): store hostinfo as a view, not as a mutable struct.
hi := *b.hostinfo // shallow copy hi := *b.hostinfo // shallow copy
unlock.UnlockEarly()
// Make a shallow copy of hostinfo so we can mutate // Make a shallow copy of hostinfo so we can mutate
// at the Service field. // at the Service field.
if !b.shouldUploadServices() { if f, ok := b.extHost.Hooks().ShouldUploadServices.GetOk(); !ok || !f() {
hi.Services = []tailcfg.Service{} hi.Services = []tailcfg.Service{}
} }
unlock.UnlockEarly()
// Don't mutate hi.Service's underlying array. Append to // Don't mutate hi.Service's underlying array. Append to
// the slice with no free capacity. // the slice with no free capacity.
c := len(hi.Services) c := len(hi.Services)

View File

@@ -5816,7 +5816,6 @@ func newLocalBackendWithSysAndTestControl(t *testing.T, enableLogging bool, sys
t.Fatalf("NewLocalBackend: %v", err) t.Fatalf("NewLocalBackend: %v", err)
} }
t.Cleanup(b.Shutdown) t.Cleanup(b.Shutdown)
b.DisablePortPollerForTest()
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) { b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
return newControl(t, opts), nil return newControl(t, opts), nil

View File

@@ -258,6 +258,12 @@ func (nb *nodeBackend) PeersForTest() []tailcfg.NodeView {
return ret return ret
} }
func (nb *nodeBackend) CollectServices() bool {
nb.mu.Lock()
defer nb.mu.Unlock()
return nb.netMap != nil && nb.netMap.CollectServices
}
// AppendMatchingPeers returns base with all peers that match pred appended. // AppendMatchingPeers returns base with all peers that match pred appended.
// //
// It acquires b.mu to read the netmap but releases it before calling pred. // It acquires b.mu to read the netmap but releases it before calling pred.

View File

@@ -358,7 +358,6 @@ func TestStateMachine(t *testing.T) {
t.Fatalf("NewLocalBackend: %v", err) t.Fatalf("NewLocalBackend: %v", err)
} }
t.Cleanup(b.Shutdown) t.Cleanup(b.Shutdown)
b.DisablePortPollerForTest()
var cc, previousCC *mockControl var cc, previousCC *mockControl
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) { b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {

View File

@@ -45,7 +45,6 @@ func newBackend(opts *options) *ipnlocal.LocalBackend {
tb.Fatalf("NewLocalBackend: %v", err) tb.Fatalf("NewLocalBackend: %v", err)
} }
tb.Cleanup(b.Shutdown) tb.Cleanup(b.Shutdown)
b.DisablePortPollerForTest()
b.SetControlClientGetterForTesting(opts.MakeControlClient) b.SetControlClientGetterForTesting(opts.MakeControlClient)
return b return b
} }

View File

@@ -251,7 +251,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+ tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+
tailscale.com/ipn/ipnstate from tailscale.com/client/local+ tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/ipn/localapi from tailscale.com/tsnet tailscale.com/ipn/localapi from tailscale.com/tsnet
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
@@ -288,7 +287,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/net/netknob from tailscale.com/logpolicy+ tailscale.com/net/netknob from tailscale.com/logpolicy+
💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+ 💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
tailscale.com/net/netutil from tailscale.com/client/local+ tailscale.com/net/netutil from tailscale.com/client/local+
tailscale.com/net/netx from tailscale.com/control/controlclient+ tailscale.com/net/netx from tailscale.com/control/controlclient+
tailscale.com/net/packet from tailscale.com/ipn/ipnlocal+ tailscale.com/net/packet from tailscale.com/ipn/ipnlocal+
@@ -312,7 +310,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/net/udprelay/status from tailscale.com/client/local tailscale.com/net/udprelay/status from tailscale.com/client/local
tailscale.com/omit from tailscale.com/ipn/conffile tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/local+ tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/posture from tailscale.com/ipn/ipnlocal tailscale.com/posture from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+ tailscale.com/proxymap from tailscale.com/tsd+
💣 tailscale.com/safesocket from tailscale.com/client/local+ 💣 tailscale.com/safesocket from tailscale.com/client/local+
@@ -356,7 +353,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/util/cmpver from tailscale.com/clientupdate+ tailscale.com/util/cmpver from tailscale.com/clientupdate+
tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+ tailscale.com/util/ctxkey from tailscale.com/client/tailscale/apitype+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
LA 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+ LA 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/appc+ tailscale.com/util/dnsname from tailscale.com/appc+
tailscale.com/util/eventbus from tailscale.com/ipn/localapi+ tailscale.com/util/eventbus from tailscale.com/ipn/localapi+
tailscale.com/util/execqueue from tailscale.com/appc+ tailscale.com/util/execqueue from tailscale.com/appc+

View File

@@ -43,6 +43,7 @@ import (
"tailscale.com/net/netns" "tailscale.com/net/netns"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/tstest/deptest"
"tailscale.com/tstest/integration" "tailscale.com/tstest/integration"
"tailscale.com/tstest/integration/testcontrol" "tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/key" "tailscale.com/types/key"
@@ -1302,3 +1303,15 @@ func mustDirect(t *testing.T, logf logger.Logf, lc1, lc2 *local.Client) {
} }
t.Error("magicsock did not find a direct path from lc1 to lc2") t.Error("magicsock did not find a direct path from lc1 to lc2")
} }
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "linux",
GOARCH: "amd64",
OnDep: func(dep string) {
if strings.Contains(dep, "portlist") {
t.Errorf("unexpected dep: %q", dep)
}
},
}.Check(t)
}