ipn/ipnlocal: add peerapi endpoint for relay server endpoint allocation

Relay server initialization is performed as part of handling peerapi
endpoint allocation requests in similar fashion to SSH server
init. The relay server is not supported on iOS for now.

Updates tailscale/corp#27502

Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
Jordan Whited 2025-04-09 13:23:21 -07:00
parent 9ff9c5af04
commit 559e548e8b
No known key found for this signature in database
GPG Key ID: 33DF352F65991EB8
9 changed files with 167 additions and 0 deletions

View File

@ -883,6 +883,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/net/tsdial from tailscale.com/control/controlclient+ tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/tsd+ tailscale.com/net/tstun from tailscale.com/tsd+
tailscale.com/net/udprelay from tailscale.com/ipn/ipnlocal
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/ipn/ipnlocal

View File

@ -333,6 +333,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+ tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+ tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/udprelay from tailscale.com/ipn/ipnlocal
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/ipn/ipnlocal

View File

@ -76,6 +76,7 @@ import (
"tailscale.com/net/packet" "tailscale.com/net/packet"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial" "tailscale.com/net/tsdial"
"tailscale.com/net/udprelay"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/portlist" "tailscale.com/portlist"
"tailscale.com/posture" "tailscale.com/posture"
@ -378,6 +379,7 @@ type LocalBackend struct {
// c2nUpdateStatus is the status of c2n-triggered client update. // c2nUpdateStatus is the status of c2n-triggered client update.
c2nUpdateStatus updateStatus c2nUpdateStatus updateStatus
currentUser ipnauth.Actor currentUser ipnauth.Actor
relayServer relayServer // or nil, initialized lazily
// backgroundProfileResolvers are optional background profile resolvers. // backgroundProfileResolvers are optional background profile resolvers.
backgroundProfileResolvers set.HandleSet[profileResolver] backgroundProfileResolvers set.HandleSet[profileResolver]
@ -1135,6 +1137,10 @@ func (b *LocalBackend) Shutdown() {
b.sshServer.Shutdown() b.sshServer.Shutdown()
b.sshServer = nil b.sshServer = nil
} }
if b.relayServer != nil {
b.relayServer.Close()
b.relayServer = nil
}
b.closePeerAPIListenersLocked() b.closePeerAPIListenersLocked()
if b.debugSink != nil { if b.debugSink != nil {
b.e.InstallCaptureHook(nil) b.e.InstallCaptureHook(nil)
@ -4609,6 +4615,17 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
b.sshServer = nil b.sshServer = nil
} }
} }
if oldp.RelayServerPort().Valid() && (newp.RelayServerPort == nil ||
oldp.RelayServerPort().Get() != *newp.RelayServerPort) {
if b.relayServer != nil {
b.goTracker.Go(func() {
b.relayServer.Close()
})
b.relayServer = nil
}
}
if netMap != nil { if netMap != nil {
newProfile := profileFromView(netMap.UserProfiles[netMap.User()]) newProfile := profileFromView(netMap.UserProfiles[netMap.User()])
if newLoginName := newProfile.LoginName; newLoginName != "" { if newLoginName := newProfile.LoginName; newLoginName != "" {
@ -6060,6 +6077,40 @@ func (b *LocalBackend) resetAuthURLLocked() {
b.authActor = nil b.authActor = nil
} }
// relayServer is the interface of the conditionally linked net/udprelay.Server.
type relayServer interface {
// AllocateEndpoint allocates a udprelay.ServerEndpoint for the provided
// pair of key.DiscoPublic's. It returns an error (udprelay.ErrServerClosed)
// if the server has been closed.
AllocateEndpoint(discoA, discoB key.DiscoPublic) (udprelay.ServerEndpoint, error)
Close() error
}
type newRelayServerFunc func(port int, addrs []netip.Addr) (relayServer, int, error)
var newRelayServer newRelayServerFunc // or nil
func registerNewRelayServer(fn newRelayServerFunc) {
newRelayServer = fn
}
func (b *LocalBackend) relayServerOrInit() (_ relayServer, err error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.relayServer != nil {
return b.relayServer, nil
}
if newRelayServer == nil {
return nil, errors.New("no relay server support")
}
b.relayServer, _, err = newRelayServer(b.Prefs().RelayServerPort().Get(), []netip.Addr{netip.MustParseAddr("127.0.0.1")})
if err != nil {
return nil, fmt.Errorf("newRelayServer: %w", err)
}
return b.relayServer, nil
}
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() } func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
// ShouldRunWebClient reports whether the web client is being run // ShouldRunWebClient reports whether the web client is being run

View File

@ -38,6 +38,7 @@ import (
"tailscale.com/net/sockstats" "tailscale.com/net/sockstats"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/taildrop" "tailscale.com/taildrop"
"tailscale.com/types/key"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/httphdr" "tailscale.com/util/httphdr"
@ -388,6 +389,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
metricIngressCalls.Add(1) metricIngressCalls.Add(1)
h.handleServeIngress(w, r) h.handleServeIngress(w, r)
return return
case "/v0/relay/endpoint":
h.handleServeRelayAllocateEndpoint(w, r)
return
} }
if ph, ok := peerAPIHandlers[r.URL.Path]; ok { if ph, ok := peerAPIHandlers[r.URL.Path]; ok {
ph(h, w, r) ph(h, w, r)
@ -1194,6 +1198,55 @@ func parseDriveFileExtensionForLog(path string) string {
return fileExt return fileExt
} }
func (h *peerAPIHandler) handleServeRelayAllocateEndpoint(w http.ResponseWriter, r *http.Request) {
logAndError := func(code int, publicMsg string) {
h.logf("relay: error (status=%d) handling request from %v: %s", code, h.remoteAddr, publicMsg)
http.Error(w, publicMsg, code)
}
if !h.ps.b.ShouldRunRelayServer() {
logAndError(http.StatusNotFound, "relay not enabled")
return
}
if !h.PeerCaps().HasCapability(tailcfg.PeerCapabilityRelay) {
logAndError(http.StatusForbidden, "relay not permitted")
return
}
if r.Method != httpm.POST {
logAndError(http.StatusMethodNotAllowed, "only POST method is allowed")
return
}
var allocateEndpointReq struct {
DiscoKeys []key.DiscoPublic
}
err := json.NewDecoder(io.LimitReader(r.Body, 512)).Decode(&allocateEndpointReq)
if err != nil {
logAndError(http.StatusBadRequest, err.Error())
return
}
if len(allocateEndpointReq.DiscoKeys) != 2 {
logAndError(http.StatusBadRequest, "2 disco public keys must be supplied")
return
}
rs, err := h.ps.b.relayServerOrInit()
if err != nil {
logAndError(http.StatusInternalServerError, "error")
return
}
ep, err := rs.AllocateEndpoint(allocateEndpointReq.DiscoKeys[0], allocateEndpointReq.DiscoKeys[1])
if err != nil {
logAndError(http.StatusInternalServerError, err.Error())
return
}
err = json.NewEncoder(w).Encode(&ep)
if err != nil {
logAndError(http.StatusInternalServerError, err.Error())
}
}
// newFakePeerAPIListener creates a new net.Listener that acts like // newFakePeerAPIListener creates a new net.Listener that acts like
// it's listening on the provided IP address and on TCP port 1. // it's listening on the provided IP address and on TCP port 1.
// //

View File

@ -0,0 +1,39 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package ipnlocal
import (
"net/netip"
"tailscale.com/envknob"
"tailscale.com/net/udprelay"
"tailscale.com/tailcfg"
)
func init() {
// Initialize the relay server constructor on all platforms except iOS (see
// build tag at top of file) for now as to limit the impact to binary size
// and resulting effect of pushing up against NetworkExtension limits.
// Eventually we will want to support the relay server on iOS, specifically
// on the Apple TV. Apple TVs are well-fitted to act as underlay relay
// servers as they are effectively always-on servers.
registerNewRelayServer(func(port int, addrs []netip.Addr) (relayServer, int, error) {
return udprelay.NewServer(port, addrs)
})
}
// ShouldRunRelayServer returns true if a relay server port has been set in prefs,
// TAILSCALE_USE_WIP_CODE environment variable is set, and the node has the
// tailcfg.NodeAttrRelayServer tailcfg.NodeCapability.
//
// TODO(jwhited): remove the envknob guard once APIs (peerapi endpoint,
// new disco message types) are stable.
func (b *LocalBackend) ShouldRunRelayServer() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.Prefs().RelayServerPort().Valid() && envknob.UseWIPCode() &&
b.netMap != nil && b.netMap.SelfNode.HasCap(tailcfg.NodeAttrRelayServer)
}

10
ipn/ipnlocal/relay_ios.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios
package ipnlocal
func (b *LocalBackend) ShouldRunRelayServer() bool {
return false
}

View File

@ -1,6 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
// Package udprelay contains constructs for relaying Disco and WireGuard packets // Package udprelay contains constructs for relaying Disco and WireGuard packets
// between Tailscale clients over UDP. This package is currently considered // between Tailscale clients over UDP. This package is currently considered
// experimental. // experimental.

View File

@ -1,6 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package udprelay package udprelay
import ( import (

View File

@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios
package udprelay
type ServerEndpoint struct{}