2025-04-16 09:50:48 -07:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
// Package relayserver registers the relay server feature and implements its
|
|
|
|
// associated ipnext.Extension.
|
|
|
|
package relayserver
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"sync"
|
|
|
|
|
2025-07-21 10:02:37 -07:00
|
|
|
"tailscale.com/disco"
|
2025-04-16 09:50:48 -07:00
|
|
|
"tailscale.com/feature"
|
2025-04-17 09:24:11 -07:00
|
|
|
"tailscale.com/ipn"
|
2025-04-16 09:50:48 -07:00
|
|
|
"tailscale.com/ipn/ipnext"
|
|
|
|
"tailscale.com/net/udprelay"
|
2025-05-09 11:29:36 -07:00
|
|
|
"tailscale.com/net/udprelay/endpoint"
|
2025-04-16 09:50:48 -07:00
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
"tailscale.com/types/key"
|
|
|
|
"tailscale.com/types/logger"
|
2025-04-17 09:24:11 -07:00
|
|
|
"tailscale.com/types/ptr"
|
2025-07-21 10:02:37 -07:00
|
|
|
"tailscale.com/util/eventbus"
|
|
|
|
"tailscale.com/wgengine/magicsock"
|
2025-04-16 09:50:48 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
// featureName is the name of the feature implemented by this package.
|
|
|
|
// It is also the [extension] name and the log prefix.
|
|
|
|
const featureName = "relayserver"
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
feature.Register(featureName)
|
|
|
|
ipnext.RegisterExtension(featureName, newExtension)
|
|
|
|
}
|
|
|
|
|
|
|
|
// newExtension is an [ipnext.NewExtensionFn] that creates a new relay server
|
|
|
|
// extension. It is registered with [ipnext.RegisterExtension] if the package is
|
|
|
|
// imported.
|
2025-07-21 10:02:37 -07:00
|
|
|
func newExtension(logf logger.Logf, sb ipnext.SafeBackend) (ipnext.Extension, error) {
|
|
|
|
return &extension{
|
|
|
|
logf: logger.WithPrefix(logf, featureName+": "),
|
|
|
|
bus: sb.Sys().Bus.Get(),
|
|
|
|
}, nil
|
2025-04-16 09:50:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// extension is an [ipnext.Extension] managing the relay server on platforms
|
|
|
|
// that import this package.
|
|
|
|
type extension struct {
|
|
|
|
logf logger.Logf
|
2025-07-21 10:02:37 -07:00
|
|
|
bus *eventbus.Bus
|
2025-04-16 09:50:48 -07:00
|
|
|
|
2025-07-21 10:02:37 -07:00
|
|
|
mu sync.Mutex // guards the following fields
|
|
|
|
eventClient *eventbus.Client // closed to stop consumeEventbusTopics
|
|
|
|
reqSub *eventbus.Subscriber[magicsock.UDPRelayAllocReq] // receives endpoint alloc requests from magicsock
|
|
|
|
respPub *eventbus.Publisher[magicsock.UDPRelayAllocResp] // publishes endpoint alloc responses to magicsock
|
2025-07-04 12:48:38 -04:00
|
|
|
shutdown bool
|
2025-07-21 10:02:37 -07:00
|
|
|
port *int // ipn.Prefs.RelayServerPort, nil if disabled
|
|
|
|
busDoneCh chan struct{} // non-nil if port is non-nil, closed when consumeEventbusTopics returns
|
|
|
|
hasNodeAttrDisableRelayServer bool // tailcfg.NodeAttrDisableRelayServer
|
|
|
|
server relayServer // lazily initialized
|
|
|
|
|
2025-04-17 09:24:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// relayServer is the interface of [udprelay.Server].
|
|
|
|
type relayServer interface {
|
2025-05-09 11:29:36 -07:00
|
|
|
AllocateEndpoint(discoA key.DiscoPublic, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error)
|
2025-04-17 09:24:11 -07:00
|
|
|
Close() error
|
2025-04-16 09:50:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Name implements [ipnext.Extension].
|
|
|
|
func (e *extension) Name() string {
|
|
|
|
return featureName
|
|
|
|
}
|
|
|
|
|
|
|
|
// Init implements [ipnext.Extension] by registering callbacks and providers
|
|
|
|
// for the duration of the extension's lifetime.
|
2025-04-17 09:24:11 -07:00
|
|
|
func (e *extension) Init(host ipnext.Host) error {
|
|
|
|
profile, prefs := host.Profiles().CurrentProfileState()
|
|
|
|
e.profileStateChanged(profile, prefs, false)
|
2025-04-25 07:37:15 -07:00
|
|
|
host.Hooks().ProfileStateChange.Add(e.profileStateChanged)
|
2025-05-07 09:15:33 -07:00
|
|
|
host.Hooks().OnSelfChange.Add(e.selfNodeViewChanged)
|
2025-04-16 09:50:48 -07:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-07-21 10:02:37 -07:00
|
|
|
// initBusConnection initializes the [*eventbus.Client], [*eventbus.Subscriber],
|
|
|
|
// [*eventbus.Publisher], and [chan struct{}] used to publish/receive endpoint
|
|
|
|
// allocation messages to/from the [*eventbus.Bus]. It also starts
|
|
|
|
// consumeEventbusTopics in a separate goroutine.
|
|
|
|
func (e *extension) initBusConnection() {
|
|
|
|
e.eventClient = e.bus.Client("relayserver.extension")
|
|
|
|
e.reqSub = eventbus.Subscribe[magicsock.UDPRelayAllocReq](e.eventClient)
|
|
|
|
e.respPub = eventbus.Publish[magicsock.UDPRelayAllocResp](e.eventClient)
|
|
|
|
e.busDoneCh = make(chan struct{})
|
|
|
|
go e.consumeEventbusTopics()
|
|
|
|
}
|
|
|
|
|
2025-05-07 09:15:33 -07:00
|
|
|
func (e *extension) selfNodeViewChanged(nodeView tailcfg.NodeView) {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
2025-07-04 12:48:38 -04:00
|
|
|
e.hasNodeAttrDisableRelayServer = nodeView.HasCap(tailcfg.NodeAttrDisableRelayServer)
|
|
|
|
if e.hasNodeAttrDisableRelayServer && e.server != nil {
|
2025-05-07 09:15:33 -07:00
|
|
|
e.server.Close()
|
|
|
|
e.server = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-17 09:24:11 -07:00
|
|
|
func (e *extension) profileStateChanged(_ ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
newPort, ok := prefs.RelayServerPort().GetOk()
|
|
|
|
enableOrDisableServer := ok != (e.port != nil)
|
|
|
|
portChanged := ok && e.port != nil && newPort != *e.port
|
|
|
|
if enableOrDisableServer || portChanged || !sameNode {
|
|
|
|
if e.server != nil {
|
|
|
|
e.server.Close()
|
|
|
|
e.server = nil
|
|
|
|
}
|
2025-07-21 10:02:37 -07:00
|
|
|
if e.port != nil {
|
|
|
|
e.eventClient.Close()
|
|
|
|
<-e.busDoneCh
|
|
|
|
}
|
2025-04-17 09:24:11 -07:00
|
|
|
e.port = nil
|
|
|
|
if ok {
|
|
|
|
e.port = ptr.To(newPort)
|
2025-07-21 10:02:37 -07:00
|
|
|
e.initBusConnection()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *extension) consumeEventbusTopics() {
|
|
|
|
defer close(e.busDoneCh)
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-e.reqSub.Done():
|
|
|
|
// If reqSub is done, the eventClient has been closed, which is a
|
|
|
|
// signal to return.
|
|
|
|
return
|
|
|
|
case req := <-e.reqSub.Events():
|
|
|
|
rs, err := e.relayServerOrInit()
|
|
|
|
if err != nil {
|
|
|
|
e.logf("error initializing server: %v", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
se, err := rs.AllocateEndpoint(req.Message.ClientDisco[0], req.Message.ClientDisco[1])
|
|
|
|
if err != nil {
|
|
|
|
e.logf("error allocating endpoint: %v", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
e.respPub.Publish(magicsock.UDPRelayAllocResp{
|
|
|
|
ReqRxFromNodeKey: req.RxFromNodeKey,
|
|
|
|
ReqRxFromDiscoKey: req.RxFromDiscoKey,
|
|
|
|
Message: &disco.AllocateUDPRelayEndpointResponse{
|
|
|
|
Generation: req.Message.Generation,
|
|
|
|
UDPRelayEndpoint: disco.UDPRelayEndpoint{
|
|
|
|
ServerDisco: se.ServerDisco,
|
|
|
|
ClientDisco: se.ClientDisco,
|
|
|
|
LamportID: se.LamportID,
|
|
|
|
VNI: se.VNI,
|
|
|
|
BindLifetime: se.BindLifetime.Duration,
|
|
|
|
SteadyStateLifetime: se.SteadyStateLifetime.Duration,
|
|
|
|
AddrPorts: se.AddrPorts,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2025-04-17 09:24:11 -07:00
|
|
|
}
|
|
|
|
}
|
2025-07-21 10:02:37 -07:00
|
|
|
|
2025-04-17 09:24:11 -07:00
|
|
|
}
|
|
|
|
|
2025-04-16 09:50:48 -07:00
|
|
|
// Shutdown implements [ipnlocal.Extension].
|
|
|
|
func (e *extension) Shutdown() error {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
e.shutdown = true
|
|
|
|
if e.server != nil {
|
|
|
|
e.server.Close()
|
|
|
|
e.server = nil
|
|
|
|
}
|
2025-07-21 10:02:37 -07:00
|
|
|
if e.port != nil {
|
|
|
|
e.eventClient.Close()
|
|
|
|
<-e.busDoneCh
|
|
|
|
}
|
2025-04-16 09:50:48 -07:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-04-17 09:24:11 -07:00
|
|
|
func (e *extension) relayServerOrInit() (relayServer, error) {
|
2025-04-16 09:50:48 -07:00
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
if e.shutdown {
|
|
|
|
return nil, errors.New("relay server is shutdown")
|
|
|
|
}
|
|
|
|
if e.server != nil {
|
|
|
|
return e.server, nil
|
|
|
|
}
|
2025-04-17 09:24:11 -07:00
|
|
|
if e.port == nil {
|
|
|
|
return nil, errors.New("relay server is not configured")
|
|
|
|
}
|
2025-07-04 12:48:38 -04:00
|
|
|
if e.hasNodeAttrDisableRelayServer {
|
|
|
|
return nil, errors.New("disable-relay-server node attribute is present")
|
2025-04-17 09:24:11 -07:00
|
|
|
}
|
2025-04-16 09:50:48 -07:00
|
|
|
var err error
|
2025-07-02 20:38:39 -07:00
|
|
|
e.server, err = udprelay.NewServer(e.logf, *e.port, nil)
|
2025-04-16 09:50:48 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return e.server, nil
|
|
|
|
}
|