From 559e548e8b73c1c28fd30f84fc3967b5a988c222 Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Wed, 9 Apr 2025 13:23:21 -0700 Subject: [PATCH] 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 --- cmd/k8s-operator/depaware.txt | 1 + cmd/tailscaled/depaware.txt | 1 + ipn/ipnlocal/local.go | 51 ++++++++++++++++++ ipn/ipnlocal/peerapi.go | 53 +++++++++++++++++++ ipn/ipnlocal/relay_default.go | 39 ++++++++++++++ ipn/ipnlocal/relay_ios.go | 10 ++++ net/udprelay/{server.go => server_default.go} | 2 + ...{server_test.go => server_default_test.go} | 2 + net/udprelay/server_ios.go | 8 +++ 9 files changed, 167 insertions(+) create mode 100644 ipn/ipnlocal/relay_default.go create mode 100644 ipn/ipnlocal/relay_ios.go rename net/udprelay/{server.go => server_default.go} (99%) rename net/udprelay/{server_test.go => server_default_test.go} (99%) create mode 100644 net/udprelay/server_ios.go diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index 7fd4c4b21..87dfe9815 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -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/tshttpproxy from tailscale.com/clientupdate/distsign+ 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/paths from tailscale.com/client/local+ 💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 394056295..64c8a4892 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -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/tshttpproxy from tailscale.com/clientupdate/distsign+ 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/paths from tailscale.com/client/local+ 💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 7d69b884d..59ff81bf8 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -76,6 +76,7 @@ import ( "tailscale.com/net/packet" "tailscale.com/net/tsaddr" "tailscale.com/net/tsdial" + "tailscale.com/net/udprelay" "tailscale.com/paths" "tailscale.com/portlist" "tailscale.com/posture" @@ -378,6 +379,7 @@ type LocalBackend struct { // c2nUpdateStatus is the status of c2n-triggered client update. c2nUpdateStatus updateStatus currentUser ipnauth.Actor + relayServer relayServer // or nil, initialized lazily // backgroundProfileResolvers are optional background profile resolvers. backgroundProfileResolvers set.HandleSet[profileResolver] @@ -1135,6 +1137,10 @@ func (b *LocalBackend) Shutdown() { b.sshServer.Shutdown() b.sshServer = nil } + if b.relayServer != nil { + b.relayServer.Close() + b.relayServer = nil + } b.closePeerAPIListenersLocked() if b.debugSink != nil { b.e.InstallCaptureHook(nil) @@ -4609,6 +4615,17 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce) 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 { newProfile := profileFromView(netMap.UserProfiles[netMap.User()]) if newLoginName := newProfile.LoginName; newLoginName != "" { @@ -6060,6 +6077,40 @@ func (b *LocalBackend) resetAuthURLLocked() { 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() } // ShouldRunWebClient reports whether the web client is being run diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 21b808fd5..71c412a93 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -38,6 +38,7 @@ import ( "tailscale.com/net/sockstats" "tailscale.com/tailcfg" "tailscale.com/taildrop" + "tailscale.com/types/key" "tailscale.com/types/views" "tailscale.com/util/clientmetric" "tailscale.com/util/httphdr" @@ -388,6 +389,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { metricIngressCalls.Add(1) h.handleServeIngress(w, r) return + case "/v0/relay/endpoint": + h.handleServeRelayAllocateEndpoint(w, r) + return } if ph, ok := peerAPIHandlers[r.URL.Path]; ok { ph(h, w, r) @@ -1194,6 +1198,55 @@ func parseDriveFileExtensionForLog(path string) string { 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 // it's listening on the provided IP address and on TCP port 1. // diff --git a/ipn/ipnlocal/relay_default.go b/ipn/ipnlocal/relay_default.go new file mode 100644 index 000000000..b7398bee7 --- /dev/null +++ b/ipn/ipnlocal/relay_default.go @@ -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) +} diff --git a/ipn/ipnlocal/relay_ios.go b/ipn/ipnlocal/relay_ios.go new file mode 100644 index 000000000..696f3abab --- /dev/null +++ b/ipn/ipnlocal/relay_ios.go @@ -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 +} diff --git a/net/udprelay/server.go b/net/udprelay/server_default.go similarity index 99% rename from net/udprelay/server.go rename to net/udprelay/server_default.go index 30fc08326..8a2ddeda0 100644 --- a/net/udprelay/server.go +++ b/net/udprelay/server_default.go @@ -1,6 +1,8 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause +//go:build !ios + // Package udprelay contains constructs for relaying Disco and WireGuard packets // between Tailscale clients over UDP. This package is currently considered // experimental. diff --git a/net/udprelay/server_test.go b/net/udprelay/server_default_test.go similarity index 99% rename from net/udprelay/server_test.go rename to net/udprelay/server_default_test.go index 733e50b77..0d99dc8c2 100644 --- a/net/udprelay/server_test.go +++ b/net/udprelay/server_default_test.go @@ -1,6 +1,8 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause +//go:build !ios + package udprelay import ( diff --git a/net/udprelay/server_ios.go b/net/udprelay/server_ios.go new file mode 100644 index 000000000..07c68571e --- /dev/null +++ b/net/udprelay/server_ios.go @@ -0,0 +1,8 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build ios + +package udprelay + +type ServerEndpoint struct{}