Skip to content

Commit

Permalink
tailcfg,ipn/ipnlocal: add C2NUpdateRequest to c2n /update handler
Browse files Browse the repository at this point in the history
An optional (for now) request body for POST /update messages to carry
the suggested version. This removes the need from clients to figure out
what the latest available version is.

Updates #6907

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
  • Loading branch information
awly committed Aug 31, 2023
1 parent 04e1ce0 commit 8e5fff7
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 2 deletions.
2 changes: 1 addition & 1 deletion cmd/tailscaled/depaware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
Expand Down
44 changes: 43 additions & 1 deletion ipn/ipnlocal/c2n.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"sync"
"time"

"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/cmpver"
"tailscale.com/util/goroutines"
"tailscale.com/version"
)
Expand Down Expand Up @@ -121,6 +124,15 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
return
}

var req tailcfg.C2NUpdateRequest
if r.ContentLength != 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
b.logf("c2n: bad /update request body: %v", err)
http.Error(w, "failed to parse request body", http.StatusBadRequest)
return
}
}

// If NewUpdater does not return an error, we can update the installation.
// Exception: When version.IsMacSysExt returns true, we don't support that
// yet. TODO(cpalmer, #6995): Implement it.
Expand All @@ -139,8 +151,16 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
}()

if r.Method == "GET" {
if req.LatestVersion != "" {
if err := validateUpdateVersion(req.LatestVersion, false); err != nil {
res.Err = fmt.Sprintf("LatestVersion is invalid: %v", err)
return
}
// TODO(awly): store req.LatestVersion somewhere to surface in UI.
}
return
}

if !res.Enabled {
res.Err = "not enabled"
return
Expand Down Expand Up @@ -171,7 +191,17 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
res.Err = "cmd/tailscale version mismatch"
return
}
cmd := exec.Command(cmdTS, "update", "--yes")
var cmd *exec.Cmd
if req.LatestVersion != "" {
if err := validateUpdateVersion(req.LatestVersion, true); err != nil {
res.Err = err.Error()
return
}
b.logf("c2n: update to version %q requested", req.LatestVersion)
cmd = exec.Command(cmdTS, "update", "--yes", "--version", req.LatestVersion)
} else {
cmd = exec.Command(cmdTS, "update", "--yes")
}
if err := cmd.Start(); err != nil {
res.Err = fmt.Sprintf("failed to start cmd/tailscale update: %v", err)
return
Expand All @@ -189,6 +219,18 @@ func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
return
}

var versionRe = sync.OnceValue(func() *regexp.Regexp { return regexp.MustCompile(`^\d+\.\d+\.\d+$`) })

func validateUpdateVersion(ver string, expectNewer bool) error {
if !versionRe().MatchString(ver) {
return fmt.Errorf("requested update version %q is malformed", ver)
}
if expectNewer && cmpver.Compare(ver, version.Short()) <= 0 {
return fmt.Errorf("requested update version %q is older or the same as current version %q", ver, version.Short())
}
return nil
}

// findCmdTailscale looks for the cmd/tailscale that corresponds to the
// currently running cmd/tailscaled. It's up to the caller to verify that the
// two match, but this function does its best to find the right one. Notably, it
Expand Down
12 changes: 12 additions & 0 deletions tailcfg/c2ntypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ type C2NSSHUsernamesResponse struct {
Usernames []string
}

// C2NUpdateRequest is the request (from control to node) to the c2n /update
// handler. It tells the node what the latest version is. The node may choose
// to ignore this update, notify the user, or apply it automatically (for POST
// requests only) based on configuration.
type C2NUpdateRequest struct {
// LatestVersion is the latest released version in the "x.y.z" format.
//
// The node should not blindly trust this version and perform validation
// against downgrades and malformed version strings.
LatestVersion string
}

// C2NUpdateResponse is the response (from node to control) from the /update
// handler. It tells control the status of its request for the node to update
// its Tailscale installation.
Expand Down

0 comments on commit 8e5fff7

Please sign in to comment.