diff options
Diffstat (limited to 'feature')
| -rw-r--r-- | feature/relayserver/relayserver.go | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/feature/relayserver/relayserver.go b/feature/relayserver/relayserver.go index b90a62345..93481080e 100644 --- a/feature/relayserver/relayserver.go +++ b/feature/relayserver/relayserver.go @@ -6,14 +6,19 @@ package relayserver import ( + "encoding/json" + "fmt" + "net/http" "sync" "tailscale.com/disco" "tailscale.com/feature" "tailscale.com/ipn" "tailscale.com/ipn/ipnext" + "tailscale.com/ipn/localapi" "tailscale.com/net/udprelay" "tailscale.com/net/udprelay/endpoint" + "tailscale.com/net/udprelay/status" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -29,6 +34,37 @@ const featureName = "relayserver" func init() { feature.Register(featureName) ipnext.RegisterExtension(featureName, newExtension) + localapi.Register("debug-peer-relay-sessions", servePeerRelayDebugSessions) +} + +// servePeerRelayDebugSessions is an HTTP handler for the Local API that +// returns debug/status information for peer relay sessions being relayed by +// this Tailscale node. It writes a JSON-encoded [status.ServerStatus] into the +// HTTP response, or returns an HTTP 405/500 with error text as the body. +func servePeerRelayDebugSessions(h *localapi.Handler, w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "GET required", http.StatusMethodNotAllowed) + return + } + + var e *extension + if ok := h.LocalBackend().FindMatchingExtension(&e); !ok { + http.Error(w, "peer relay server extension unavailable", http.StatusInternalServerError) + return + } + + st, err := e.status() + if err != nil { + http.Error(w, fmt.Sprintf("failed to retrieve peer relay server status: %v", err), http.StatusInternalServerError) + return + } + + j, err := json.Marshal(st) + if err != nil { + http.Error(w, fmt.Sprintf("failed to marshal json: %v", err), http.StatusInternalServerError) + return + } + w.Write(j) } // newExtension is an [ipnext.NewExtensionFn] that creates a new relay server @@ -59,6 +95,27 @@ type extension struct { type relayServer interface { AllocateEndpoint(discoA key.DiscoPublic, discoB key.DiscoPublic) (endpoint.ServerEndpoint, error) Close() error + GetSessions() ([]status.ServerSession, error) +} + +// PeerRelaySessionsReq is an empty event bus message type, used to send an +// async request for peer relay status information to the peer relay server's +// event loop. The server should respond with a [PeerRelaySessionsResp] via the +// event bus. +type PeerRelaySessionsReq struct{} + +// PeerRelaySessionsResp is an event bus message type containing peer relay +// status information. Sent by the peer relay server in response to a +// [PeerRelaySessionsReq] message. +type PeerRelaySessionsResp struct { + // Status is the current status/config of the peer relay server and all of + // its peer relay sessions (if any). May be the zero value if Error is + // populated. + Status status.ServerStatus + // Error contains any error generated by the peer relay server while trying + // to gather status; it may or may not be populated regardless of whether + // the Status field is valid. + Error error } // Name implements [ipnext.Extension]. @@ -119,6 +176,8 @@ func (e *extension) consumeEventbusTopics(port int) { defer close(e.busDoneCh) eventClient := e.bus.Client("relayserver.extension") + debugReqSub := eventbus.Subscribe[PeerRelaySessionsReq](eventClient) + debugRespPub := eventbus.Publish[PeerRelaySessionsResp](eventClient) reqSub := eventbus.Subscribe[magicsock.UDPRelayAllocReq](eventClient) respPub := eventbus.Publish[magicsock.UDPRelayAllocResp](eventClient) defer eventClient.Close() @@ -137,6 +196,32 @@ func (e *extension) consumeEventbusTopics(port int) { // If reqSub is done, the eventClient has been closed, which is a // signal to return. return + case <-debugReqSub.Events(): + st := status.ServerStatus{ + State: status.Uninitialized, + UDPPort: port, + Sessions: nil, + } + if rs == nil { + // Don't initialize the server simply for a debug request; + // return the status as-is. + resp := PeerRelaySessionsResp{st, nil} + debugRespPub.Publish(resp) + continue + } + // We know the server is [status.Running] because rs != nil, which + // can only be the case if the port is configured and peer relaying + // isn't disabled by node attribute. + st.State = status.Running + sessions, err := rs.GetSessions() + if err != nil { + prs_err := fmt.Errorf("error retrieving peer relay sessions: %v", err) + e.logf(prs_err.Error()) + debugRespPub.Publish(PeerRelaySessionsResp{Error: prs_err}) + continue + } + st.Sessions = sessions + debugRespPub.Publish(PeerRelaySessionsResp{st, nil}) case req := <-reqSub.Events(): if rs == nil { var err error @@ -188,3 +273,52 @@ func (e *extension) Shutdown() error { e.shutdown = true return nil } + +// status gathers and returns current peer relay server status information for +// this Tailscale node, including if this node is disabled/not configured as a +// peer relay server, and status of each peer relay session this node is +// relaying (if any). +func (e *extension) status() (status.ServerStatus, error) { + st := status.ServerStatus{ + State: status.Uninitialized, + UDPPort: -1, + Sessions: nil, + } + + e.mu.Lock() + running := e.busDoneCh != nil + shutdown := e.shutdown + port := e.port + disabled := e.hasNodeAttrDisableRelayServer + e.mu.Unlock() + + if port == nil { + st.State = status.NotConfigured + return st, nil + } + + st.UDPPort = *port + if disabled { + st.State = status.Disabled + return st, nil + } + + if shutdown { + st.State = status.ShutDown + return st, nil + } + + if !running { + // Leave state as Uninitialized. + return st, nil + } + + client := e.bus.Client("relayserver.debug-peer-relay-sessions") + defer client.Close() + debugReqPub := eventbus.Publish[PeerRelaySessionsReq](client) + debugRespSub := eventbus.Subscribe[PeerRelaySessionsResp](client) + + debugReqPub.Publish(PeerRelaySessionsReq{}) + resp := <-debugRespSub.Events() + return resp.Status, resp.Error +} |
