瀏覽代碼

all: add means to set device posture attributes from node

Updates tailscale/corp#24690
Updates #4077

Change-Id: I05fe799beb1d2a71d1ec3ae08744cc68bcadae2a
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 1 年之前
父節點
當前提交
ff095606cc
共有 5 個文件被更改,包括 125 次插入2 次删除
  1. 50 0
      control/controlclient/direct.go
  2. 5 2
      control/controlclient/noise.go
  3. 14 0
      ipn/ipnlocal/local.go
  4. 28 0
      ipn/localapi/localapi.go
  5. 28 0
      tailcfg/tailcfg.go

+ 50 - 0
control/controlclient/direct.go

@@ -1643,6 +1643,56 @@ func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyStat
 	res.Body.Close()
 }
 
+// SetDeviceAttrs does a synchronous call to the control plane to update
+// the node's attributes.
+//
+// See docs on [tailcfg.SetDeviceAttributesRequest] for background.
+func (c *Auto) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
+	return c.direct.SetDeviceAttrs(ctx, attrs)
+}
+
+// SetDeviceAttrs does a synchronous call to the control plane to update
+// the node's attributes.
+//
+// See docs on [tailcfg.SetDeviceAttributesRequest] for background.
+func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
+	nc, err := c.getNoiseClient()
+	if err != nil {
+		return err
+	}
+	nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
+	if !ok {
+		return errors.New("no node key")
+	}
+	if c.panicOnUse {
+		panic("tainted client")
+	}
+	req := &tailcfg.SetDeviceAttributesRequest{
+		NodeKey: nodeKey,
+		Version: tailcfg.CurrentCapabilityVersion,
+		Update:  attrs,
+	}
+
+	// TODO(bradfitz): unify the callers using doWithBody vs those using
+	// DoNoiseRequest. There seems to be a ~50/50 split and they're very close,
+	// but doWithBody sets the load balancing header and auto-JSON-encodes the
+	// body, but DoNoiseRequest is exported. Clean it up so they're consistent
+	// one way or another.
+
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+	res, err := nc.doWithBody(ctx, "PATCH", "/machine/set-device-attr", nodeKey, req)
+	if err != nil {
+		return err
+	}
+	defer res.Body.Close()
+	all, _ := io.ReadAll(res.Body)
+	if res.StatusCode != 200 {
+		return fmt.Errorf("HTTP error from control plane: %v: %s", res.Status, all)
+	}
+	return nil
+}
+
 func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
 	if !nodeKey.IsZero() {
 		req.Header.Add(tailcfg.LBHeader, nodeKey.String())

+ 5 - 2
control/controlclient/noise.go

@@ -380,17 +380,20 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseconn.Conn, error) {
 // post does a POST to the control server at the given path, JSON-encoding body.
 // The provided nodeKey is an optional load balancing hint.
 func (nc *NoiseClient) post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
+	return nc.doWithBody(ctx, "POST", path, nodeKey, body)
+}
+
+func (nc *NoiseClient) doWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
 	jbody, err := json.Marshal(body)
 	if err != nil {
 		return nil, err
 	}
-	req, err := http.NewRequestWithContext(ctx, "POST", "https://"+nc.host+path, bytes.NewReader(jbody))
+	req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
 	if err != nil {
 		return nil, err
 	}
 	addLBHeader(req, nodeKey)
 	req.Header.Set("Content-Type", "application/json")
-
 	conn, err := nc.getConn(ctx)
 	if err != nil {
 		return nil, err

+ 14 - 0
ipn/ipnlocal/local.go

@@ -6408,6 +6408,20 @@ func (b *LocalBackend) SetExpirySooner(ctx context.Context, expiry time.Time) er
 	return cc.SetExpirySooner(ctx, expiry)
 }
 
+// SetDeviceAttrs does a synchronous call to the control plane to update
+// the node's attributes.
+//
+// See docs on [tailcfg.SetDeviceAttributesRequest] for background.
+func (b *LocalBackend) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
+	b.mu.Lock()
+	cc := b.ccAuto
+	b.mu.Unlock()
+	if cc == nil {
+		return errors.New("not running")
+	}
+	return cc.SetDeviceAttrs(ctx, attrs)
+}
+
 // exitNodeCanProxyDNS reports the DoH base URL ("http://foo/dns-query") without query parameters
 // to exitNodeID's DoH service, if available.
 //

+ 28 - 0
ipn/localapi/localapi.go

@@ -83,6 +83,7 @@ var handler = map[string]localAPIHandler{
 
 	// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
 	// without a trailing slash:
+	"alpha-set-device-attrs":      (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690
 	"bugreport":                   (*Handler).serveBugReport,
 	"check-ip-forwarding":         (*Handler).serveCheckIPForwarding,
 	"check-prefs":                 (*Handler).serveCheckPrefs,
@@ -446,6 +447,33 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
 	h.serveWhoIsWithBackend(w, r, h.b)
 }
 
+// serveSetDeviceAttrs is (as of 2024-12-30) an experimental LocalAPI handler to
+// set device attributes via the control plane.
+//
+// See tailscale/corp#24690.
+func (h *Handler) serveSetDeviceAttrs(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	if !h.PermitWrite {
+		http.Error(w, "set-device-attrs access denied", http.StatusForbidden)
+		return
+	}
+	if r.Method != "PATCH" {
+		http.Error(w, "only PATCH allowed", http.StatusMethodNotAllowed)
+		return
+	}
+	var req map[string]any
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	if err := h.b.SetDeviceAttrs(ctx, req); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.Header().Set("Content-Type", "application/json")
+	io.WriteString(w, "{}\n")
+}
+
 // localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed
 // by the localapi WhoIs method.
 type localBackendWhoIsMethods interface {

+ 28 - 0
tailcfg/tailcfg.go

@@ -2455,6 +2455,34 @@ type HealthChangeRequest struct {
 	NodeKey key.NodePublic
 }
 
+// SetDeviceAttributesRequest is a request to update the
+// current node's device posture attributes.
+//
+// As of 2024-12-30, this is an experimental dev feature
+// for internal testing. See tailscale/corp#24690.
+type SetDeviceAttributesRequest struct {
+	// Version is the current binary's [CurrentCapabilityVersion].
+	Version CapabilityVersion
+
+	// NodeKey identifies the node to modify. It should be the currently active
+	// node and is an error if not.
+	NodeKey key.NodePublic
+
+	// Update is a map of device posture attributes to update.
+	// Attributes not in the map are left unchanged.
+	Update AttrUpdate
+}
+
+// AttrUpdate is a map of attributes to update.
+// Attributes not in the map are left unchanged.
+// The value can be a string, float64, bool, or nil to delete.
+//
+// See https://tailscale.com/s/api-device-posture-attrs.
+//
+// TODO(bradfitz): add struct type for specifying optional associated data
+// for each attribute value, like an expiry time?
+type AttrUpdate map[string]any
+
 // SSHPolicy is the policy for how to handle incoming SSH connections
 // over Tailscale.
 type SSHPolicy struct {