mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-16 03:31:39 +00:00
ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update (#10188)
* ipn/{ipnlocal,ipnstate,localapi}: add localapi endpoints for client self-update Updates #10187. Signed-off-by: Naman Sood <mail@nsood.in> * depaware Updates #10187. Signed-off-by: Naman Sood <mail@nsood.in> * address review feedback Signed-off-by: Naman Sood <mail@nsood.in> --------- Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
parent
55cd5c575b
commit
e57fd9cda4
@ -228,7 +228,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/client/tailscale from tailscale.com/derp+
|
tailscale.com/client/tailscale from tailscale.com/derp+
|
||||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
|
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
|
||||||
tailscale.com/clientupdate from tailscale.com/ipn/ipnlocal
|
tailscale.com/clientupdate from tailscale.com/ipn/ipnlocal+
|
||||||
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||||
tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+
|
tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh+
|
||||||
tailscale.com/control/controlbase from tailscale.com/control/controlclient+
|
tailscale.com/control/controlbase from tailscale.com/control/controlclient+
|
||||||
|
@ -34,6 +34,7 @@ import (
|
|||||||
"gvisor.dev/gvisor/pkg/tcpip"
|
"gvisor.dev/gvisor/pkg/tcpip"
|
||||||
"tailscale.com/appc"
|
"tailscale.com/appc"
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
|
"tailscale.com/clientupdate"
|
||||||
"tailscale.com/control/controlclient"
|
"tailscale.com/control/controlclient"
|
||||||
"tailscale.com/control/controlknobs"
|
"tailscale.com/control/controlknobs"
|
||||||
"tailscale.com/doctor"
|
"tailscale.com/doctor"
|
||||||
@ -267,6 +268,8 @@ type LocalBackend struct {
|
|||||||
// c2nUpdateStatus is the status of c2n-triggered client update.
|
// c2nUpdateStatus is the status of c2n-triggered client update.
|
||||||
c2nUpdateStatus updateStatus
|
c2nUpdateStatus updateStatus
|
||||||
currentUser ipnauth.WindowsToken
|
currentUser ipnauth.WindowsToken
|
||||||
|
selfUpdateProgress []ipnstate.UpdateProgress
|
||||||
|
lastSelfUpdateState ipnstate.SelfUpdateStatus
|
||||||
|
|
||||||
// ServeConfig fields. (also guarded by mu)
|
// ServeConfig fields. (also guarded by mu)
|
||||||
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
||||||
@ -374,6 +377,8 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
|||||||
loginFlags: loginFlags,
|
loginFlags: loginFlags,
|
||||||
clock: clock,
|
clock: clock,
|
||||||
activeWatchSessions: make(set.Set[string]),
|
activeWatchSessions: make(set.Set[string]),
|
||||||
|
selfUpdateProgress: make([]ipnstate.UpdateProgress, 0),
|
||||||
|
lastSelfUpdateState: ipnstate.UpdateFinished,
|
||||||
}
|
}
|
||||||
|
|
||||||
netMon := sys.NetMon.Get()
|
netMon := sys.NetMon.Get()
|
||||||
@ -5539,6 +5544,54 @@ func (b *LocalBackend) DebugBreakDERPConns() error {
|
|||||||
return b.magicConn().DebugBreakDERPConns()
|
return b.magicConn().DebugBreakDERPConns()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) pushSelfUpdateProgress(up ipnstate.UpdateProgress) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.selfUpdateProgress = append(b.selfUpdateProgress, up)
|
||||||
|
b.lastSelfUpdateState = up.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) clearSelfUpdateProgress() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.selfUpdateProgress = make([]ipnstate.UpdateProgress, 0)
|
||||||
|
b.lastSelfUpdateState = ipnstate.UpdateFinished
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) GetSelfUpdateProgress() []ipnstate.UpdateProgress {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
res := make([]ipnstate.UpdateProgress, len(b.selfUpdateProgress))
|
||||||
|
copy(res, b.selfUpdateProgress)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) DoSelfUpdate() {
|
||||||
|
b.mu.Lock()
|
||||||
|
updateState := b.lastSelfUpdateState
|
||||||
|
b.mu.Unlock()
|
||||||
|
// don't start an update if one is already in progress
|
||||||
|
if updateState == ipnstate.UpdateInProgress {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.clearSelfUpdateProgress()
|
||||||
|
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, ""))
|
||||||
|
up, err := clientupdate.NewUpdater(clientupdate.Arguments{
|
||||||
|
Logf: func(format string, args ...any) {
|
||||||
|
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateInProgress, fmt.Sprintf(format, args...)))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
|
||||||
|
}
|
||||||
|
err = up.Update()
|
||||||
|
if err != nil {
|
||||||
|
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFailed, err.Error()))
|
||||||
|
} else {
|
||||||
|
b.pushSelfUpdateProgress(ipnstate.NewUpdateProgress(ipnstate.UpdateFinished, "tailscaled did not restart; please restart Tailscale manually."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ObserveDNSResponse passes a DNS response from the PeerAPI DNS server to the
|
// ObserveDNSResponse passes a DNS response from the PeerAPI DNS server to the
|
||||||
// App Connector to enable route discovery.
|
// App Connector to enable route discovery.
|
||||||
func (b *LocalBackend) ObserveDNSResponse(res []byte) {
|
func (b *LocalBackend) ObserveDNSResponse(res []byte) {
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
"tailscale.com/util/dnsname"
|
"tailscale.com/util/dnsname"
|
||||||
|
"tailscale.com/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TKAFilteredPeer
|
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TKAFilteredPeer
|
||||||
@ -710,3 +711,25 @@ type DebugDERPRegionReport struct {
|
|||||||
Warnings []string
|
Warnings []string
|
||||||
Errors []string
|
Errors []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelfUpdateStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UpdateFinished SelfUpdateStatus = "UpdateFinished"
|
||||||
|
UpdateInProgress SelfUpdateStatus = "UpdateInProgress"
|
||||||
|
UpdateFailed SelfUpdateStatus = "UpdateFailed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateProgress struct {
|
||||||
|
Status SelfUpdateStatus `json:"status,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateProgress(ps SelfUpdateStatus, msg string) UpdateProgress {
|
||||||
|
return UpdateProgress{
|
||||||
|
Status: ps,
|
||||||
|
Message: msg,
|
||||||
|
Version: version.Short(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/client/tailscale/apitype"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
|
"tailscale.com/clientupdate"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
@ -125,6 +126,9 @@ var handler = map[string]localAPIHandler{
|
|||||||
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
|
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
|
||||||
"whois": (*Handler).serveWhoIs,
|
"whois": (*Handler).serveWhoIs,
|
||||||
"query-feature": (*Handler).serveQueryFeature,
|
"query-feature": (*Handler).serveQueryFeature,
|
||||||
|
"update/check": (*Handler).serveUpdateCheck,
|
||||||
|
"update/install": (*Handler).serveUpdateInstall,
|
||||||
|
"update/progress": (*Handler).serveUpdateProgress,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -2418,6 +2422,75 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serveUpdateCheck returns the ClientVersion from Status, which contains
|
||||||
|
// information on whether an update is available, and if so, what version,
|
||||||
|
// *if* we support auto-updates on this platform. If we don't, this endpoint
|
||||||
|
// always returns a ClientVersion saying we're running the newest version.
|
||||||
|
// Effectively, it tells us whether serveUpdateInstall will be able to install
|
||||||
|
// an update for us.
|
||||||
|
func (h *Handler) serveUpdateCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := clientupdate.NewUpdater(clientupdate.Arguments{
|
||||||
|
ForAutoUpdate: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// if we don't support auto-update, just say that we're up to date
|
||||||
|
if errors.Is(err, errors.ErrUnsupported) {
|
||||||
|
json.NewEncoder(w).Encode(tailcfg.ClientVersion{RunningLatest: true})
|
||||||
|
} else {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cv := h.b.StatusWithoutPeers().ClientVersion
|
||||||
|
// ipnstate.Status documentation notes that ClientVersion may be nil on some
|
||||||
|
// platforms where this information is unavailable. In that case, return a
|
||||||
|
// ClientVersion that says we're up to date, since we have no information on
|
||||||
|
// whether an update is possible.
|
||||||
|
if cv == nil {
|
||||||
|
cv = &tailcfg.ClientVersion{RunningLatest: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(cv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveUpdateInstall sends a request to the LocalBackend to start a Tailscale
|
||||||
|
// self-update. A successful response does not indicate whether the update
|
||||||
|
// succeeded, only that the request was accepted. Clients should use
|
||||||
|
// serveUpdateProgress after pinging this endpoint to check how the update is
|
||||||
|
// going.
|
||||||
|
func (h *Handler) serveUpdateInstall(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
|
||||||
|
go h.b.DoSelfUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveUpdateProgress returns the status of an in-progress Tailscale self-update.
|
||||||
|
// This is provided as a slice of ipnstate.UpdateProgress structs with various
|
||||||
|
// log messages in order from oldest to newest. If an update is not in progress,
|
||||||
|
// the returned slice will be empty.
|
||||||
|
func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ups := h.b.GetSelfUpdateProgress()
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(ups)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")
|
metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user