diff --git a/client/local/local.go b/client/local/local.go index e72589306..75fdbe5a5 100644 --- a/client/local/local.go +++ b/client/local/local.go @@ -1422,3 +1422,13 @@ func (lc *Client) GetAppConnectorRouteInfo(ctx context.Context) (appctype.RouteI } return decodeJSON[appctype.RouteInfo](body) } + +// GetServices returns the Services visible to this node, +// including their names, IP addresses, and ports, keyed by service name. +func (lc *Client) GetServices(ctx context.Context) (map[tailcfg.ServiceName]tailcfg.ServiceDetails, error) { + body, err := lc.get200(ctx, "/localapi/v0/services") + if err != nil { + return nil, err + } + return decodeJSON[map[tailcfg.ServiceName]tailcfg.ServiceDetails](body) +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 43942c52f..b06b69b04 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -82,6 +82,7 @@ var handler = map[string]LocalAPIHandler{ "prefs": (*Handler).servePrefs, "reload-config": (*Handler).reloadConfig, "reset-auth": (*Handler).serveResetAuth, + "services": (*Handler).serveServices, "set-expiry-sooner": (*Handler).serveSetExpirySooner, "shutdown": (*Handler).serveShutdown, "start": (*Handler).serveStart, @@ -1707,6 +1708,20 @@ func (h *Handler) serveShutdown(w http.ResponseWriter, r *http.Request) { eventbus.Publish[Shutdown](ec).Publish(Shutdown{}) } +func (h *Handler) serveServices(w http.ResponseWriter, r *http.Request) { + if r.Method != httpm.GET { + http.Error(w, "only GET allowed", http.StatusMethodNotAllowed) + return + } + nm := h.b.NetMap() + if nm == nil { + http.Error(w, "no netmap", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(nm.Services()) +} + func (h *Handler) serveGetAppcRouteInfo(w http.ResponseWriter, r *http.Request) { if !buildfeatures.HasAppConnectors { http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented) diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index 90ed9b3fc..ef76c05da 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -149,6 +149,9 @@ func (nm *NetworkMap) GetIPVIPServiceMap() IPServiceMappings { // Services returns the Services visible (accessible) to this node, // decoded from [tailcfg.NodeAttrPrefixServices]+serviceName entries in the // self node's CapMap. Returns nil if nm is nil or SelfNode is invalid. +// +// TODO(adrianosela): cache the result of decoding the capmap so +// we don't have to decode it multiple times after each netmap update. func (nm *NetworkMap) Services() map[tailcfg.ServiceName]tailcfg.ServiceDetails { if nm == nil || !nm.SelfNode.Valid() { return nil