diff --git a/build_dist.sh b/build_dist.sh index c01670398..5b1ca75b2 100755 --- a/build_dist.sh +++ b/build_dist.sh @@ -41,7 +41,7 @@ while [ "$#" -gt 1 ]; do fi shift ldflags="$ldflags -w -s" - tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture" + tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture,ts_omit_relayserver" ;; --box) if [ ! -z "${TAGS:-}" ]; then diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index 416265188..7af6a7c10 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -806,6 +806,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/ tailscale.com/feature from tailscale.com/feature/wakeonlan+ tailscale.com/feature/capture from tailscale.com/feature/condregister tailscale.com/feature/condregister from tailscale.com/tsnet + tailscale.com/feature/relayserver from tailscale.com/feature/condregister L tailscale.com/feature/tap from tailscale.com/feature/condregister tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister tailscale.com/health from tailscale.com/control/controlclient+ @@ -816,7 +817,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/ tailscale.com/ipn from tailscale.com/client/local+ tailscale.com/ipn/conffile from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+ - tailscale.com/ipn/ipnext from tailscale.com/ipn/ipnlocal + tailscale.com/ipn/ipnext from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+ tailscale.com/ipn/ipnstate from tailscale.com/client/local+ tailscale.com/ipn/localapi from tailscale.com/tsnet+ @@ -883,6 +884,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/feature/relayserver 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 9cdebbae1..28a597065 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -264,6 +264,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/feature from tailscale.com/feature/wakeonlan+ tailscale.com/feature/capture from tailscale.com/feature/condregister tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled + tailscale.com/feature/relayserver from tailscale.com/feature/condregister L tailscale.com/feature/tap from tailscale.com/feature/condregister tailscale.com/feature/wakeonlan from tailscale.com/feature/condregister tailscale.com/health from tailscale.com/control/controlclient+ @@ -334,6 +335,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/feature/relayserver 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/feature/condregister/maybe_relayserver.go b/feature/condregister/maybe_relayserver.go new file mode 100644 index 000000000..3360dd062 --- /dev/null +++ b/feature/condregister/maybe_relayserver.go @@ -0,0 +1,8 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !ios && !ts_omit_relayserver + +package condregister + +import _ "tailscale.com/feature/relayserver" diff --git a/feature/relayserver/relayserver.go b/feature/relayserver/relayserver.go new file mode 100644 index 000000000..9cf776661 --- /dev/null +++ b/feature/relayserver/relayserver.go @@ -0,0 +1,154 @@ +// 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 ( + "encoding/json" + "errors" + "io" + "net/http" + "net/netip" + "sync" + + "tailscale.com/feature" + "tailscale.com/ipn/ipnext" + "tailscale.com/ipn/ipnlocal" + "tailscale.com/net/udprelay" + "tailscale.com/tailcfg" + "tailscale.com/tsd" + "tailscale.com/types/key" + "tailscale.com/types/logger" + "tailscale.com/util/httpm" +) + +// 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) + ipnlocal.RegisterPeerAPIHandler("/v0/relay/endpoint", handlePeerAPIRelayAllocateEndpoint) +} + +// newExtension is an [ipnext.NewExtensionFn] that creates a new relay server +// extension. It is registered with [ipnext.RegisterExtension] if the package is +// imported. +func newExtension(logf logger.Logf, _ *tsd.System) (ipnext.Extension, error) { + return &extension{logf: logger.WithPrefix(logf, featureName+": ")}, nil +} + +// extension is an [ipnext.Extension] managing the relay server on platforms +// that import this package. +type extension struct { + logf logger.Logf + + mu sync.Mutex // guards the following fields + shutdown bool + port int + server *udprelay.Server // lazily initialized +} + +// 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. +func (e *extension) Init(_ ipnext.Host) error { + return nil +} + +// 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 + } + return nil +} + +func (e *extension) shouldRunRelayServer() bool { + // TODO(jwhited): consider: + // 1. tailcfg.NodeAttrRelayServer + // 2. ipn.Prefs.RelayServerPort + // 3. envknob.UseWIPCode() + // 4. e.shutdown + return false +} + +func (e *extension) relayServerOrInit() (*udprelay.Server, error) { + 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 + } + var err error + e.server, _, err = udprelay.NewServer(e.port, []netip.Addr{netip.MustParseAddr("127.0.0.1")}) + if err != nil { + return nil, err + } + return e.server, nil +} + +func handlePeerAPIRelayAllocateEndpoint(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) { + // TODO(jwhited): log errors + e, ok := h.LocalBackend().FindExtensionByName(featureName).(*extension) + if !ok { + http.Error(w, "relay failed to initialize", http.StatusServiceUnavailable) + return + } + + if !e.shouldRunRelayServer() { + http.Error(w, "relay not enabled", http.StatusNotFound) + return + } + + if !h.PeerCaps().HasCapability(tailcfg.PeerCapabilityRelay) { + http.Error(w, "relay not permitted", http.StatusForbidden) + return + } + + if r.Method != httpm.POST { + http.Error(w, "only POST method is allowed", http.StatusMethodNotAllowed) + return + } + + var allocateEndpointReq struct { + DiscoKeys []key.DiscoPublic + } + err := json.NewDecoder(io.LimitReader(r.Body, 512)).Decode(&allocateEndpointReq) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if len(allocateEndpointReq.DiscoKeys) != 2 { + http.Error(w, "2 disco public keys must be supplied", http.StatusBadRequest) + return + } + + rs, err := e.relayServerOrInit() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ep, err := rs.AllocateEndpoint(allocateEndpointReq.DiscoKeys[0], allocateEndpointReq.DiscoKeys[1]) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = json.NewEncoder(w).Encode(&ep) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +}