From 51a410523acd4687a91ff6b48e05a4c7d711126a Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Sun, 28 Apr 2019 12:27:06 +0200 Subject: updater: add initial skeleton Signed-off-by: Jason A. Donenfeld --- installer/build.bat | 3 - installer/wireguard.wxs | 2 +- ui/iconprovider.go | 55 ++++++++++++- ui/managewindow.go | 19 ++++- ui/tray.go | 13 ++++ ui/tunnelsview.go | 2 +- ui/ui.go | 41 ++++++++-- ui/updatepage.go | 106 +++++++++++++++++++++++++ updater/constants.go | 14 ++++ updater/downloader.go | 181 +++++++++++++++++++++++++++++++++++++++++++ updater/msirunner_linux.go | 21 +++++ updater/msirunner_windows.go | 32 ++++++++ updater/signify.go | 73 +++++++++++++++++ updater/updater_test.go | 41 ++++++++++ updater/versions.go | 85 ++++++++++++++++++++ version.h | 2 +- version/mksyscall.go | 8 ++ version/os_linux.go | 30 +++++++ version/os_windows.go | 43 ++++++++++ version/version.go | 20 +++++ version/zsyscall_windows.go | 49 ++++++++++++ 21 files changed, 820 insertions(+), 20 deletions(-) create mode 100644 ui/updatepage.go create mode 100644 updater/constants.go create mode 100644 updater/downloader.go create mode 100644 updater/msirunner_linux.go create mode 100644 updater/msirunner_windows.go create mode 100644 updater/signify.go create mode 100644 updater/updater_test.go create mode 100644 updater/versions.go create mode 100644 version/mksyscall.go create mode 100644 version/os_linux.go create mode 100644 version/os_windows.go create mode 100644 version/version.go create mode 100644 version/zsyscall_windows.go diff --git a/installer/build.bat b/installer/build.bat index c7322857..918f932a 100644 --- a/installer/build.bat +++ b/installer/build.bat @@ -5,9 +5,6 @@ rem Copyright (C) 2019 WireGuard LLC. All Rights Reserved. for /f "tokens=3" %%a in ('findstr /r "[0-9.]*" ..\version.h') do set WIREGUARD_VERSION=%%a set WIREGUARD_VERSION=%WIREGUARD_VERSION:"=% -rem While our version numbers remain whonky, just hardcode this to a version that makes msi always replace. -set WIREGUARD_VERSION=0.0.0.1 - set STARTDIR=%cd% set OLDWIX=%WIX% set WIX_CANDLE_FLAGS=-nologo -dWIREGUARD_VERSION="%WIREGUARD_VERSION%" diff --git a/installer/wireguard.wxs b/installer/wireguard.wxs index 49ba8f45..da31c3c6 100644 --- a/installer/wireguard.wxs +++ b/installer/wireguard.wxs @@ -38,7 +38,7 @@ Upgrading --> diff --git a/ui/iconprovider.go b/ui/iconprovider.go index 56b8cfbc..01c10675 100644 --- a/ui/iconprovider.go +++ b/ui/iconprovider.go @@ -26,12 +26,14 @@ type IconProvider struct { stoppedPen *walk.CosmeticPen startingPen *walk.CosmeticPen startedPen *walk.CosmeticPen + updateAvailableImage *walk.Bitmap } const ( - colorStopped = 0xe1e1e1 - colorStarting = 0xfec031 - colorStarted = 0x36ce42 + colorStopped = 0xe1e1e1 + colorStarting = 0xfec031 + colorStarted = 0x36ce42 + colorUpdateAvailable = 0xf03a17 ) func hexColor(c uint32) walk.Color { @@ -152,8 +154,12 @@ func NewIconProvider() (*IconProvider, error) { } disposables.Add(tsip.startedPen) - disposables.Spare() + if tsip.updateAvailableImage, err = tsip.drawUpdateAvailableImage(16); err != nil { + return nil, err + } + disposables.Add(tsip.updateAvailableImage) + disposables.Spare() return tsip, nil } @@ -198,6 +204,47 @@ func (tsip *IconProvider) Dispose() { tsip.baseIcon.Dispose() tsip.baseIcon = nil } + if tsip.updateAvailableImage != nil { + tsip.updateAvailableImage.Dispose() + tsip.updateAvailableImage = nil + } +} + +func (tsip *IconProvider) drawUpdateAvailableImage(size int) (*walk.Bitmap, error) { + updateAvailableBrush, err := walk.NewSolidColorBrush(hexColor(colorUpdateAvailable)) + if err != nil { + return nil, err + } + defer updateAvailableBrush.Dispose() + updateAvailablePen, err := walk.NewCosmeticPen(walk.PenSolid, darkColor(hexColor(colorUpdateAvailable))) + if err != nil { + return nil, err + } + defer updateAvailablePen.Dispose() + + img, err := walk.NewBitmapWithTransparentPixels(walk.Size{size, size}) + if err != nil { + return nil, err + } + + canvas, err := walk.NewCanvasFromImage(img) + if err != nil { + img.Dispose() + return nil, err + } + defer canvas.Dispose() + + rect := walk.Rectangle{0, 0, size, size} + + if err := canvas.FillEllipse(updateAvailableBrush, rect); err != nil { + img.Dispose() + return nil, err + } + if err := canvas.DrawEllipse(updateAvailablePen, rect); err != nil { + img.Dispose() + return nil, err + } + return img, nil } func (tsip *IconProvider) ImageForTunnel(tunnel *service.Tunnel, size walk.Size) (*walk.Bitmap, error) { diff --git a/ui/managewindow.go b/ui/managewindow.go index c4d6dc3c..4046d6db 100644 --- a/ui/managewindow.go +++ b/ui/managewindow.go @@ -14,8 +14,10 @@ import ( type ManageTunnelsWindow struct { *walk.MainWindow + tabs *walk.TabWidget tunnelsPage *TunnelsPage logPage *LogPage + updatePage *UpdatePage tunnelChangedCB *service.TunnelChangeCallback } @@ -59,13 +61,13 @@ func NewManageTunnelsWindow() (*ManageTunnelsWindow, error) { } }) - tabWidget, _ := walk.NewTabWidget(mtw) + mtw.tabs, _ = walk.NewTabWidget(mtw) mtw.tunnelsPage, _ = NewTunnelsPage() - tabWidget.Pages().Add(mtw.tunnelsPage.TabPage) + mtw.tabs.Pages().Add(mtw.tunnelsPage.TabPage) mtw.logPage, _ = NewLogPage() - tabWidget.Pages().Add(mtw.logPage.TabPage) + mtw.tabs.Pages().Add(mtw.logPage.TabPage) disposables.Spare() @@ -102,3 +104,14 @@ func (mtw *ManageTunnelsWindow) onTunnelChange(tunnel *service.Tunnel, state ser } }) } + +func (mtw *ManageTunnelsWindow) UpdateFound() { + if mtw.updatePage != nil { + return + } + updatePage, err := NewUpdatePage() + if err == nil { + mtw.updatePage = updatePage + mtw.tabs.Pages().Add(updatePage.TabPage) + } +} diff --git a/ui/tray.go b/ui/tray.go index 22c37ab4..ca839a83 100644 --- a/ui/tray.go +++ b/ui/tray.go @@ -278,3 +278,16 @@ func (tray *Tray) SetTunnelState(tunnel *service.Tunnel, state service.TunnelSta tray.updateGlobalState() } + +func (tray *Tray) UpdateFound() { + action := walk.NewAction() + action.SetText("An Update is Available!") + action.SetImage(iconProvider.updateAvailableImage) + //TODO: Make bold + action.Triggered().Attach(func() { + tray.mtw.Show() + tray.mtw.tabs.SetCurrentIndex(2) + }) + tray.ContextMenu().Actions().Insert(tray.ContextMenu().Actions().Len()-2, action) + tray.ShowWarning("WireGuard Update Available", "An update to WireGuard is now available. You are advised to update as soon as possible.") +} diff --git a/ui/tunnelsview.go b/ui/tunnelsview.go index 5ad733f9..2a963b58 100644 --- a/ui/tunnelsview.go +++ b/ui/tunnelsview.go @@ -115,7 +115,7 @@ func (tv *TunnelsView) StyleCell(style *walk.CellStyle) { b.X = b.Height b.Width -= b.Height - canvas.DrawText(tunnel.Name, tv.Font(), 0, b, walk.TextVCenter | walk.TextSingleLine) + canvas.DrawText(tunnel.Name, tv.Font(), 0, b, walk.TextVCenter|walk.TextSingleLine) b.X = 0 b.Width = b.Height diff --git a/ui/ui.go b/ui/ui.go index e24fc6c8..316fbefe 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -7,17 +7,17 @@ package ui import ( "fmt" - "runtime" - "runtime/debug" - "github.com/lxn/walk" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/windows/service" + "golang.zx2c4.com/wireguard/windows/updater" + "golang.zx2c4.com/wireguard/windows/version" + "log" + "runtime" + "runtime/debug" + "time" ) -// #include "../version.h" -import "C" - var iconProvider *IconProvider var shouldQuitManagerWhenExiting = false @@ -50,6 +50,31 @@ func RunUI() { panic(err) } + go func() { + first := true + for { + update, err := updater.CheckForUpdate() + if err == nil && update != nil { + mtw.Synchronize(func() { + mtw.UpdateFound() + tray.UpdateFound() + }) + return + } + if err != nil { + log.Printf("Update checker: %v", err) + if first { + time.Sleep(time.Minute * 4) + first = false + } else { + time.Sleep(time.Minute * 25) + } + } else { + time.Sleep(time.Hour) + } + } + }() + mtw.Run() tray.Dispose() mtw.Dispose() @@ -102,10 +127,12 @@ func onAbout(owner walk.Form) { detailsLbl.SetText(fmt.Sprintf(`App version: %s Go backend version: %s +Golang version: %s %s +%s Copyright © 2015-2019 WireGuard LLC. All Rights Reserved.`, - C.WIREGUARD_WINDOWS_VERSION, device.WireGuardGoVersion)) + version.WireGuardWindowsVersion, device.WireGuardGoVersion, runtime.Version(), runtime.GOARCH, version.OsName())) hbl := walk.NewHBoxLayout() hbl.SetMargins(walk.Margins{VNear: 10}) diff --git a/ui/updatepage.go b/ui/updatepage.go new file mode 100644 index 00000000..85b8558b --- /dev/null +++ b/ui/updatepage.go @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package ui + +import ( + "fmt" + "golang.zx2c4.com/wireguard/windows/updater" + + "github.com/lxn/walk" +) + +type UpdatePage struct { + *walk.TabPage +} + +func NewUpdatePage() (*UpdatePage, error) { + up := &UpdatePage{} + var err error + + if up.TabPage, err = walk.NewTabPage(); err != nil { + return nil, err + } + + up.SetTitle("An Update is Available!") + up.SetImage(iconProvider.updateAvailableImage) + //TODO: make title bold + up.SetLayout(walk.NewVBoxLayout()) + up.Layout().SetMargins(walk.Margins{18, 18, 18, 18}) + + instructions, _ := walk.NewTextLabel(up) + instructions.SetText("An update to WireGuard is available. It is highly advisable to update without delay.") + + status, _ := walk.NewTextLabel(up) + status.SetText("Status: Waiting for user") + + bar, _ := walk.NewProgressBar(up) + bar.SetVisible(false) + + button, _ := walk.NewPushButton(up) + button.SetText("Update Now") + + walk.NewVSpacer(up) + + button.Clicked().Attach(func() { + up.SetSuspended(true) + button.SetEnabled(false) + button.SetVisible(false) + bar.SetVisible(true) + bar.SetMarqueeMode(true) + up.SetSuspended(false) + progress := updater.DownloadVerifyAndExecute() + go func() { + for { + dp := <-progress + retNow := false + up.Synchronize(func() { + if dp.Error != nil { + up.SetSuspended(true) + bar.SetVisible(false) + bar.SetValue(0) + bar.SetRange(0, 1) + bar.SetMarqueeMode(false) + button.SetVisible(true) + button.SetEnabled(true) + status.SetText(fmt.Sprintf("Error: %v. Please try again.", dp.Error)) + up.SetSuspended(false) + retNow = true + return + } + if len(dp.Activity) > 0 { + status.SetText(fmt.Sprintf("Status: %s", dp.Activity)) + } + if dp.BytesTotal > 0 { + bar.SetMarqueeMode(false) + bar.SetRange(0, int(dp.BytesTotal)) + bar.SetValue(int(dp.BytesDownloaded)) + } else { + bar.SetMarqueeMode(true) + bar.SetValue(0) + bar.SetRange(0, 1) + } + if dp.Complete { + up.SetSuspended(true) + bar.SetVisible(false) + bar.SetValue(0) + bar.SetRange(0, 0) + bar.SetMarqueeMode(false) + button.SetVisible(true) + button.SetEnabled(true) + status.SetText("Status: Complete!") + up.SetSuspended(false) + retNow = true + return + } + }) + if retNow { + return + } + } + }() + }) + return up, nil +} diff --git a/updater/constants.go b/updater/constants.go new file mode 100644 index 00000000..ae3988bd --- /dev/null +++ b/updater/constants.go @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package updater + +const ( + releasePublicKeyBase64 = "RWQGxwD+15iPpnPCEijYJ3CWYFgojWwBJZNg0OnJfICVu/CfyKeQ0vIA" + latestVersionURL = "https://download.wireguard.com/windows-client/latest.sig" + msiURL = "https://download.wireguard.com/windows-client/%s" + msiArchPrefix = "wireguard-%s-" + msiSuffix = ".msi" +) diff --git a/updater/downloader.go b/updater/downloader.go new file mode 100644 index 00000000..ea3ee9d4 --- /dev/null +++ b/updater/downloader.go @@ -0,0 +1,181 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package updater + +import ( + "crypto/hmac" + "errors" + "fmt" + "golang.org/x/crypto/blake2b" + "golang.zx2c4.com/wireguard/windows/version" + "hash" + "io" + "net/http" + "os" + "path" + "sync/atomic" +) + +type DownloadProgress struct { + Activity string + BytesDownloaded uint64 + BytesTotal uint64 + Error error + Complete bool +} + +type progressHashWatcher struct { + dp *DownloadProgress + c chan DownloadProgress + hashState hash.Hash +} + +func (pm *progressHashWatcher) Write(p []byte) (int, error) { + bytes := len(p) + pm.dp.BytesDownloaded += uint64(bytes) + pm.c <- *pm.dp + pm.hashState.Write(p) + return bytes, nil +} + +type UpdateFound struct { + name string + hash [blake2b.Size256]byte +} + +func CheckForUpdate() (*UpdateFound, error) { + request, err := http.NewRequest(http.MethodGet, latestVersionURL, nil) + if err != nil { + return nil, err + } + request.Header.Add("User-Agent", version.UserAgent()) + response, err := http.DefaultClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + var fileList [1024 * 512] /* 512 KiB */ byte + bytesRead, err := response.Body.Read(fileList[:]) + if err != nil && (err != io.EOF || bytesRead == 0) { + return nil, err + } + files, err := readFileList(fileList[:bytesRead]) + if err != nil { + return nil, err + } + return findCandidate(files) +} + +var updateInProgress = uint32(0) + +func DownloadVerifyAndExecute() (progress chan DownloadProgress) { + progress = make(chan DownloadProgress, 128) + progress <- DownloadProgress{Activity: "Initializing"} + + if !atomic.CompareAndSwapUint32(&updateInProgress, 0, 1) { + progress <- DownloadProgress{Error: errors.New("An update is already in progress")} + return + } + + go func() { + defer atomic.StoreUint32(&updateInProgress, 0) + + progress <- DownloadProgress{Activity: "Rechecking for update"} + update, err := CheckForUpdate() + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + if update == nil { + progress <- DownloadProgress{Error: errors.New("No update was found when re-checking for updates")} + return + } + + progress <- DownloadProgress{Activity: "Creating update file"} + updateDir, err := msiSaveDirectory() + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + // Clean up old updates the brutal way: + os.RemoveAll(updateDir) + + err = os.MkdirAll(updateDir, 0700) + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + destinationFilename := path.Join(updateDir, update.name) + unverifiedDestinationFilename := destinationFilename + ".unverified" + out, err := os.Create(unverifiedDestinationFilename) + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + defer func() { + if out != nil { + out.Seek(0, io.SeekStart) + out.Truncate(0) + out.Close() + os.Remove(unverifiedDestinationFilename) + } + }() + + dp := DownloadProgress{Activity: "Downloading update"} + progress <- dp + request, err := http.NewRequest(http.MethodGet, fmt.Sprintf(msiURL, update.name), nil) + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + request.Header.Add("User-Agent", version.UserAgent()) + request.Header.Set("Accept-Encoding", "identity") + response, err := http.DefaultClient.Do(request) + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + defer response.Body.Close() + if response.ContentLength >= 0 { + dp.BytesTotal = uint64(response.ContentLength) + progress <- dp + } + hasher, err := blake2b.New256(nil) + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + pm := &progressHashWatcher{&dp, progress, hasher} + _, err = io.Copy(out, io.TeeReader(io.LimitReader(response.Body, 1024*1024*100 /* 100 MiB */), pm)) + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + if !hmac.Equal(hasher.Sum(nil), update.hash[:]) { + progress <- DownloadProgress{Error: errors.New("The downloaded update has the wrong hash")} + return + } + out.Close() + out = nil + err = os.Rename(unverifiedDestinationFilename, destinationFilename) + if err != nil { + os.Remove(unverifiedDestinationFilename) + progress <- DownloadProgress{Error: err} + return + } + progress <- DownloadProgress{Activity: "Installing update"} + err = runMsi(destinationFilename) + os.Remove(unverifiedDestinationFilename) + if err != nil { + progress <- DownloadProgress{Error: err} + return + } + + progress <- DownloadProgress{Complete: true} + }() + + return progress +} diff --git a/updater/msirunner_linux.go b/updater/msirunner_linux.go new file mode 100644 index 00000000..974c0883 --- /dev/null +++ b/updater/msirunner_linux.go @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package updater + +import ( + "fmt" + "os/exec" +) + +// This isn't a Linux program, yes, but having the updater package work across platforms is quite helpful for testing. + +func runMsi(msiPath string) error { + return exec.Command("qarma", "--info", "--text", fmt.Sprintf("It seems to be working! Were we on Windows, \"%s\" would be executed.", msiPath)).Run() +} + +func msiSaveDirectory() (string, error) { + return "/tmp/wireguard-update-test-msi-directory", nil +} diff --git a/updater/msirunner_windows.go b/updater/msirunner_windows.go new file mode 100644 index 00000000..a498af3c --- /dev/null +++ b/updater/msirunner_windows.go @@ -0,0 +1,32 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package updater + +import ( + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/conf" + "os/exec" + "path" +) + +func runMsi(msiPath string) error { + system32, err := windows.GetSystemDirectory() + if err != nil { + return err + } + // BUG: The Go documentation says that its built-in shell quoting isn't good for msiexec.exe. + // See https://github.com/golang/go/issues/15566. But perhaps our limited set of options + // actually works fine? Investigate this! + return exec.Command(path.Join(system32, "msiexec.exe"), "/quiet", "/i", msiPath).Run() +} + +func msiSaveDirectory() (string, error) { + configRootDir, err := conf.RootDirectory() + if err != nil { + return "", err + } + return path.Join(configRootDir, "Updates"), nil +} diff --git a/updater/signify.go b/updater/signify.go new file mode 100644 index 00000000..d4605cbb --- /dev/null +++ b/updater/signify.go @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package updater + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "errors" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/ed25519" + "strings" +) + +/* + * Generate with: + * $ b2sum -l 256 *.msi > list + * $ signify -S -e -s release.sec -m list + * $ upload ./list.sec + */ + +type fileList map[string][blake2b.Size256]byte + +func readFileList(input []byte) (fileList, error) { + publicKeyBytes, err := base64.StdEncoding.DecodeString(releasePublicKeyBase64) + if err != nil || len(publicKeyBytes) != ed25519.PublicKeySize+10 || publicKeyBytes[0] != 'E' || publicKeyBytes[1] != 'd' { + return nil, errors.New("Invalid public key") + } + publicKeyBytes = publicKeyBytes[10:] + lines := bytes.SplitN(input, []byte{'\n'}, 3) + if len(lines) != 3 { + return nil, errors.New("Signature input has too few lines") + } + if !bytes.HasPrefix(lines[0], []byte("untrusted comment: ")) { + return nil, errors.New("Signature input is missing untrusted comment") + } + signatureBytes, err := base64.StdEncoding.DecodeString(string(lines[1])) + if err != nil { + return nil, errors.New("Signature input is not valid base64") + } + if len(signatureBytes) != ed25519.SignatureSize+10 || signatureBytes[0] != 'E' || signatureBytes[1] != 'd' { + return nil, errors.New("Signature input bytes are incorrect length or represent invalid signature type") + } + signatureBytes = signatureBytes[10:] + if !ed25519.Verify(publicKeyBytes, lines[2], signatureBytes) { + return nil, errors.New("Signature is invalid") + } + fileLines := strings.Split(string(lines[2]), "\n") + fileHashes := make(map[string][blake2b.Size256]byte, len(fileLines)) + for index, line := range fileLines { + if len(line) == 0 && index == len(fileLines)-1 { + break + } + components := strings.SplitN(line, " ", 2) + if len(components) != 2 { + return nil, errors.New("File hash line has too few components") + } + maybeHash, err := hex.DecodeString(components[0]) + if err != nil || len(maybeHash) != blake2b.Size256 { + return nil, errors.New("File hash is invalid base64 or incorrect number of bytes") + } + var hash [blake2b.Size256]byte + copy(hash[:], maybeHash) + fileHashes[components[1]] = hash + } + if len(fileHashes) == 0 { + return nil, errors.New("No file hashes found in signed input") + } + return fileHashes, nil +} diff --git a/updater/updater_test.go b/updater/updater_test.go new file mode 100644 index 00000000..7bc4df8e --- /dev/null +++ b/updater/updater_test.go @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package updater + +import ( + "testing" +) + +func TestUpdate(t *testing.T) { + update, err := CheckForUpdate() + if err != nil { + t.Error(err) + return + } + if update == nil { + t.Error("No update available") + return + } + t.Log("Found update") + progress := DownloadVerifyAndExecute() + for { + dp := <-progress + if dp.Error != nil { + t.Error(dp.Error) + return + } + if len(dp.Activity) > 0 { + t.Log(dp.Activity) + } + if dp.BytesTotal > 0 { + t.Logf("Downloaded %d of %d", dp.BytesDownloaded, dp.BytesTotal) + } + if dp.Complete { + t.Log("Complete!") + break + } + } +} diff --git a/updater/versions.go b/updater/versions.go new file mode 100644 index 00000000..a5b6c258 --- /dev/null +++ b/updater/versions.go @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package updater + +import ( + "errors" + "fmt" + "golang.zx2c4.com/wireguard/windows/version" + "runtime" + "strconv" + "strings" +) + +func versionNewerThanUs(candidate string) (bool, error) { + candidateParts := strings.Split(candidate, ".") + ourParts := strings.Split(version.WireGuardWindowsVersion, ".") + if len(candidateParts) == 0 || len(ourParts) == 0 { + return false, errors.New("Empty version") + } + l := len(candidateParts) + if len(ourParts) > l { + l = len(ourParts) + } + for i := 0; i < l; i++ { + var err error + cP, oP := uint64(0), uint64(0) + if i < len(candidateParts) { + if len(candidateParts[i]) == 0 { + return false, errors.New("Empty version part") + } + cP, err = strconv.ParseUint(candidateParts[i], 10, 16) + if err != nil { + return false, errors.New("Invalid version integer part") + } + } + if i < len(ourParts) { + if len(ourParts[i]) == 0 { + return false, errors.New("Empty version part") + } + oP, err = strconv.ParseUint(ourParts[i], 10, 16) + if err != nil { + return false, errors.New("Invalid version integer part") + } + } + if cP == oP { + continue + } + return cP > oP, nil + } + return false, nil +} + +func findCandidate(candidates fileList) (*UpdateFound, error) { + var arch string + if runtime.GOARCH == "amd64" { + arch = "amd64" + } else if runtime.GOARCH == "386" { + arch = "x86" + } else if runtime.GOARCH == "arm64" { + arch = "arm64" + } else { + return nil, errors.New("Invalid GOARCH") + } + prefix := fmt.Sprintf(msiArchPrefix, arch) + suffix := msiSuffix + for name, hash := range candidates { + if strings.HasPrefix(name, prefix) && strings.HasSuffix(name, suffix) { + version := strings.TrimSuffix(strings.TrimPrefix(name, prefix), suffix) + if len(version) > 128 { + return nil, errors.New("Version length is too long") + } + newer, err := versionNewerThanUs(version) + if err != nil { + return nil, err + } + if newer { + return &UpdateFound{name, hash}, nil + } + } + } + return nil, nil +} diff --git a/version.h b/version.h index 8da652db..fc42a077 100644 --- a/version.h +++ b/version.h @@ -1 +1 @@ -#define WIREGUARD_WINDOWS_VERSION "0.0.20190307" +#define WIREGUARD_WINDOWS_VERSION "0.0.1" diff --git a/version/mksyscall.go b/version/mksyscall.go new file mode 100644 index 00000000..abd538e5 --- /dev/null +++ b/version/mksyscall.go @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package version + +//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall_windows.go os_windows.go diff --git a/version/os_linux.go b/version/os_linux.go new file mode 100644 index 00000000..7e5c4f1c --- /dev/null +++ b/version/os_linux.go @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package version + +import ( + "bytes" + "fmt" + "golang.org/x/sys/unix" +) + +// This isn't a Linux program, yes, but having the updater package work across platforms is quite helpful for testing. + +func utsToStr(u [65]byte) string { + i := bytes.IndexByte(u[:], 0) + if i < 0 { + return string(u[:]) + } + return string(u[:i]) +} + +func OsName() string { + var utsname unix.Utsname + if unix.Uname(&utsname) != nil { + return "Unix Unknown" + } + return fmt.Sprintf("%s %s %s", utsToStr(utsname.Sysname), utsToStr(utsname.Release), utsToStr(utsname.Version)) +} diff --git a/version/os_windows.go b/version/os_windows.go new file mode 100644 index 00000000..ac4e8957 --- /dev/null +++ b/version/os_windows.go @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package version + +import ( + "fmt" + "unsafe" +) + +type osVersionInfo struct { + osVersionInfoSize uint32 + majorVersion uint32 + minorVersion uint32 + buildNumber uint32 + platformId uint32 + csdVersion [128]uint16 + servicePackMajor uint16 + servicePackMinor uint16 + suiteMask uint16 + productType byte + reserved byte +} + +//sys rtlGetVersion(versionInfo *osVersionInfo) (nterr uint32) = ntdll.RtlGetVersion + +func OsName() string { + windowsVersion := "Windows Unknown" + versionInfo := &osVersionInfo{osVersionInfoSize: uint32(unsafe.Sizeof(osVersionInfo{}))} + if rtlGetVersion(versionInfo) == 0 { + winType := "" + switch versionInfo.productType { + case 3: + winType = " Server" + case 2: + winType = " Controller" + } + windowsVersion = fmt.Sprintf("Windows%s %d.%d.%d", winType, versionInfo.majorVersion, versionInfo.minorVersion, versionInfo.buildNumber) + } + return windowsVersion +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 00000000..951d2731 --- /dev/null +++ b/version/version.go @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package version + +// #include "../version.h" +import "C" +import ( + "fmt" + "golang.zx2c4.com/wireguard/device" + "runtime" +) + +const WireGuardWindowsVersion = C.WIREGUARD_WINDOWS_VERSION + +func UserAgent() string { + return fmt.Sprintf("WireGuard/%s (wireguard-go %s; %s; %s; %s)", WireGuardWindowsVersion, device.WireGuardGoVersion, OsName(), runtime.Version(), runtime.GOARCH) +} diff --git a/version/zsyscall_windows.go b/version/zsyscall_windows.go new file mode 100644 index 00000000..9f11ce70 --- /dev/null +++ b/version/zsyscall_windows.go @@ -0,0 +1,49 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package version + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return nil + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modntdll = windows.NewLazySystemDLL("ntdll.dll") + + procRtlGetVersion = modntdll.NewProc("RtlGetVersion") +) + +func rtlGetVersion(versionInfo *osVersionInfo) (nterr uint32) { + r0, _, _ := syscall.Syscall(procRtlGetVersion.Addr(), 1, uintptr(unsafe.Pointer(versionInfo)), 0, 0) + nterr = uint32(r0) + return +} -- cgit v1.2.3-59-g8ed1b