mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-08 09:07:44 +00:00
all: add means to set device posture attributes from node
Updates tailscale/corp#24690 Updates #4077 Change-Id: I05fe799beb1d2a71d1ec3ae08744cc68bcadae2a Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
30d3e7b242
commit
ff095606cc
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
//
|
||||
|
@ -83,6 +83,7 @@
|
||||
|
||||
// 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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user