ipn/localapi,client/local: add services over localapi

Updates tailscale/corp#40052

Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
This commit is contained in:
Adriano Sela Aviles 2026-04-13 08:52:10 -07:00 committed by Adriano Sela Aviles
parent aa9a76cf30
commit 21880457eb
3 changed files with 28 additions and 0 deletions

View File

@ -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)
}

View File

@ -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)

View File

@ -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