aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2019-04-28 12:27:06 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2019-04-29 08:41:30 +0200
commite9682162ac908e9b9d81f3378faf8b38d1baa630 (patch)
tree1c3e6059569376c72336ba09f06e9a5ee5e1e833
parentgo.mod: use forked winio with no thirdparty deps (diff)
downloadwireguard-windows-e9682162ac908e9b9d81f3378faf8b38d1baa630.tar.xz
wireguard-windows-e9682162ac908e9b9d81f3378faf8b38d1baa630.zip
updater: add initial skeleton
-rw-r--r--installer/build.bat3
-rw-r--r--installer/wireguard.wxs2
-rw-r--r--ui/iconprovider.go55
-rw-r--r--ui/managewindow.go19
-rw-r--r--ui/tray.go13
-rw-r--r--ui/tunnelsview.go2
-rw-r--r--ui/ui.go41
-rw-r--r--ui/updatepage.go106
-rw-r--r--updater/constants.go14
-rw-r--r--updater/downloader.go181
-rw-r--r--updater/msirunner_linux.go21
-rw-r--r--updater/msirunner_windows.go32
-rw-r--r--updater/signify.go73
-rw-r--r--updater/updater_test.go41
-rw-r--r--updater/versions.go85
-rw-r--r--version.h2
-rw-r--r--version/mksyscall.go8
-rw-r--r--version/os_linux.go30
-rw-r--r--version/os_windows.go43
-rw-r--r--version/version.go20
-rw-r--r--version/zsyscall_windows.go49
21 files changed, 820 insertions, 20 deletions
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
-->
<MajorUpgrade
- AllowDowngrades="no" AllowSameVersionUpgrades="yes"
+ AllowDowngrades="no"
DowngradeErrorMessage="A newer version of [ProductName] is already installed."
Schedule="afterInstallExecute" />
<Property Id="INSTALLFOLDERPREV">
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
+}