Browse Source

release/dist/synology: build synology packages with cmd/dist

Updates #8217

Signed-off-by: David Anderson <[email protected]>
David Anderson 2 years ago
parent
commit
32e0ba5e68

+ 2 - 3
Makefile

@@ -48,11 +48,10 @@ staticcheck: ## Run staticcheck.io checks
 	./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
 
 spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version
-	PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o tailscale.spk --source=. --goarch=${SYNO_ARCH} --dsm-version=${SYNO_DSM}
+	./tool/go run ./cmd/dist build synology/dsm${SYNO_DSM}/${SYNO_ARCH}
 
 spkall: ## Build synology packages for all architectures and DSM versions
-	mkdir -p spks
-	PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o spks --source=. --goarch=all --dsm-version=all
+	./tool/go run ./cmd/dist build synology
 
 pushspk: spk ## Push and install synology package on ${SYNO_HOST} host
 	echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."

+ 24 - 1
cmd/dist/dist.go

@@ -13,15 +13,38 @@ import (
 
 	"tailscale.com/release/dist"
 	"tailscale.com/release/dist/cli"
+	"tailscale.com/release/dist/synology"
 	"tailscale.com/release/dist/unixpkgs"
 )
 
+var synologyPackageCenter bool
+
 func getTargets() ([]dist.Target, error) {
-	return unixpkgs.Targets(), nil
+	var ret []dist.Target
+
+	ret = append(ret, unixpkgs.Targets()...)
+	// Synology packages can be built either for sideloading, or for
+	// distribution by Synology in their package center. When
+	// distributed through the package center, apps can request
+	// additional permissions to use a tuntap interface and control
+	// the NAS's network stack, rather than be forced to run in
+	// userspace mode.
+	//
+	// Since only we can provide packages to Synology for
+	// distribution, we default to building the "sideload" variant of
+	// packages that we distribute on pkgs.tailscale.com.
+	ret = append(ret, synology.Targets(synologyPackageCenter)...)
+	return ret, nil
 }
 
 func main() {
 	cmd := cli.CLI(getTargets)
+	for _, subcmd := range cmd.Subcommands {
+		if subcmd.Name == "build" {
+			subcmd.FlagSet.BoolVar(&synologyPackageCenter, "synology-package-center", false, "build synology packages with extra metadata for the official package center")
+		}
+	}
+
 	if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) {
 		log.Fatal(err)
 	}

+ 7 - 0
release/dist/dist.go

@@ -17,6 +17,7 @@ import (
 	"sort"
 	"strings"
 	"sync"
+	"time"
 
 	"tailscale.com/util/multierr"
 	"tailscale.com/version/mkversion"
@@ -44,6 +45,8 @@ type Build struct {
 	Go string
 	// Version is the version info of the build.
 	Version mkversion.VersionInfo
+	// Time is the timestamp of the build.
+	Time time.Time
 
 	// once is a cache of function invocations that should run once per process
 	// (for example building a helper docker container)
@@ -86,6 +89,7 @@ func NewBuild(repo, out string) (*Build, error) {
 		Out:          out,
 		Go:           goTool,
 		Version:      mkversion.Info(),
+		Time:         time.Now().UTC(),
 		extra:        map[any]any{},
 		goBuildLimit: make(chan struct{}, runtime.NumCPU()),
 	}
@@ -114,6 +118,9 @@ func (b *Build) Build(targets []Target) (files []string, err error) {
 		go func(i int, t Target) {
 			var err error
 			defer func() {
+				if err != nil {
+					err = fmt.Errorf("%s: %w", t, err)
+				}
 				errs[i] = err
 				wg.Done()
 			}()

BIN
release/dist/synology/files/PACKAGE_ICON.PNG


BIN
release/dist/synology/files/PACKAGE_ICON_256.PNG


+ 6 - 0
release/dist/synology/files/Tailscale.sc

@@ -0,0 +1,6 @@
+[Tailscale]
+title="Tailscale"
+desc="Tailscale VPN"
+port_forward="no"
+src.ports="41641/udp"
+dst.ports="41641/udp"

+ 12 - 0
release/dist/synology/files/config

@@ -0,0 +1,12 @@
+{
+	".url": {
+		"SYNO.SDS.Tailscale": {
+			"type": "url",
+			"version": "1.8.3",
+			"title": "Tailscale",
+			"icon": "PACKAGE_ICON_256.PNG",
+			"url": "webman/3rdparty/Tailscale/",
+			"urlTarget": "_syno_tailscale"
+		}
+	}
+}

+ 2 - 0
release/dist/synology/files/index.cgi

@@ -0,0 +1,2 @@
+#! /bin/sh
+exec /var/packages/Tailscale/target/bin/tailscale web -cgi

+ 8 - 0
release/dist/synology/files/logrotate-dsm6

@@ -0,0 +1,8 @@
+/var/packages/Tailscale/etc/tailscaled.stdout.log {
+  size 10M
+  rotate 3
+  missingok
+  copytruncate
+  compress
+  notifempty
+}

+ 8 - 0
release/dist/synology/files/logrotate-dsm7

@@ -0,0 +1,8 @@
+/var/packages/Tailscale/var/tailscaled.stdout.log {
+  size 10M
+  rotate 3
+  missingok
+  copytruncate
+  compress
+  notifempty
+}

+ 7 - 0
release/dist/synology/files/privilege-dsm6

@@ -0,0 +1,7 @@
+{
+  "defaults":{
+    "run-as": "root"
+  },
+  "username": "tailscale",
+  "groupname": "tailscale"
+}

+ 7 - 0
release/dist/synology/files/privilege-dsm7

@@ -0,0 +1,7 @@
+{
+  "defaults":{
+    "run-as": "package"
+  },
+  "username": "tailscale",
+  "groupname": "tailscale"
+}

+ 13 - 0
release/dist/synology/files/privilege-dsm7.for-package-center

@@ -0,0 +1,13 @@
+{
+  "defaults":{
+    "run-as": "package"
+  },
+  "username": "tailscale",
+  "groupname": "tailscale",
+  "tool": [{
+    "relpath": "bin/tailscaled",
+    "user": "package",
+    "group": "package",
+    "capabilities": "cap_net_admin,cap_chown,cap_net_raw"
+  }]
+}

+ 11 - 0
release/dist/synology/files/resource

@@ -0,0 +1,11 @@
+{
+	"port-config": {
+		"protocol-file": "conf/Tailscale.sc"
+	},
+	"usr-local-linker": {
+		"bin": ["bin/tailscale"]
+	},
+	"syslog-config": {
+		"logrotate-relpath": "conf/logrotate.conf"
+	}
+}

+ 3 - 0
release/dist/synology/files/scripts/postupgrade

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 0

+ 3 - 0
release/dist/synology/files/scripts/preupgrade

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 0

+ 129 - 0
release/dist/synology/files/scripts/start-stop-status

@@ -0,0 +1,129 @@
+#!/bin/bash
+
+SERVICE_NAME="tailscale"
+
+if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "6" ]; then
+    PKGVAR="/var/packages/Tailscale/etc"
+else
+    PKGVAR="${SYNOPKG_PKGVAR}"
+fi
+
+PID_FILE="${PKGVAR}/tailscaled.pid"
+LOG_FILE="${PKGVAR}/tailscaled.stdout.log"
+STATE_FILE="${PKGVAR}/tailscaled.state"
+SOCKET_FILE="${PKGVAR}/tailscaled.sock"
+PORT="41641"
+
+SERVICE_COMMAND="${SYNOPKG_PKGDEST}/bin/tailscaled \
+--state=${STATE_FILE} \
+--socket=${SOCKET_FILE} \
+--port=$PORT"
+
+if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "7" -a ! -e "/dev/net/tun" ]; then
+    # TODO(maisem/crawshaw): Disable the tun device in DSM7 for now.
+    SERVICE_COMMAND="${SERVICE_COMMAND} --tun=userspace-networking"
+fi
+
+if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "6" ]; then
+    chown -R tailscale:tailscale "${PKGVAR}/"
+fi
+
+start_daemon() {
+    local ts=$(date --iso-8601=second)
+    echo "${ts} Starting ${SERVICE_NAME} with: ${SERVICE_COMMAND}" >${LOG_FILE}
+    STATE_DIRECTORY=${PKGVAR} ${SERVICE_COMMAND} 2>&1 | sed -u '1,200p;201s,.*,[further tailscaled logs suppressed],p;d' >>${LOG_FILE} &
+    # We pipe tailscaled's output to sed, so "$!" retrieves the PID of sed not tailscaled.
+    # Use jobs -p to retrieve the PID of the most recent process group leader.
+    jobs -p >"${PID_FILE}"
+}
+
+stop_daemon() {
+    if [ -r "${PID_FILE}" ]; then
+        local PID=$(cat "${PID_FILE}")
+        local ts=$(date --iso-8601=second)
+        echo "${ts} Stopping ${SERVICE_NAME} service PID=${PID}" >>${LOG_FILE}
+        kill -TERM $PID >>${LOG_FILE} 2>&1
+        wait_for_status 1 || kill -KILL $PID >>${LOG_FILE} 2>&1
+        rm -f "${PID_FILE}" >/dev/null
+    fi
+}
+
+daemon_status() {
+    if [ -r "${PID_FILE}" ]; then
+        local PID=$(cat "${PID_FILE}")
+        if ps -o pid -p ${PID} > /dev/null; then
+            return
+        fi
+        rm -f "${PID_FILE}" >/dev/null
+    fi
+    return 1
+}
+
+wait_for_status() {
+    # 20 tries
+    # sleeps for 1 second after each try
+    local counter=20
+    while [ ${counter} -gt 0 ]; do
+        daemon_status
+        [ $? -eq $1 ] && return
+        counter=$((counter - 1))
+        sleep 1
+    done
+    return 1
+}
+
+ensure_tun_created() {
+    if [ "${SYNOPKG_DSM_VERSION_MAJOR}" -eq "7" ]; then
+        # TODO(maisem/crawshaw): Disable the tun device in DSM7 for now.
+        return
+    fi
+    # Create the necessary file structure for /dev/net/tun
+    if ([ ! -c /dev/net/tun ]); then
+        if ([ ! -d /dev/net ]); then
+            mkdir -m 755 /dev/net
+        fi
+        mknod /dev/net/tun c 10 200
+        chmod 0755 /dev/net/tun
+    fi
+
+    # Load the tun module if not already loaded
+    if (!(lsmod | grep -q "^tun\s")); then
+        insmod /lib/modules/tun.ko
+    fi
+}
+
+case $1 in
+start)
+    if daemon_status; then
+        exit 0
+    else
+        ensure_tun_created
+        start_daemon
+        exit $?
+    fi
+    ;;
+stop)
+    if daemon_status; then
+        stop_daemon
+        exit $?
+    else
+        exit 0
+    fi
+    ;;
+status)
+    if daemon_status; then
+        echo "${SERVICE_NAME} is running"
+        exit 0
+    else
+        echo "${SERVICE_NAME} is not running"
+        exit 3
+    fi
+    ;;
+log)
+    exit 0
+    ;;
+*)
+    echo "command $1 is not implemented"
+    exit 0
+    ;;
+esac

+ 306 - 0
release/dist/synology/pkgs.go

@@ -0,0 +1,306 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+// Package synology contains dist Targets for building Synology Tailscale packages.
+package synology
+
+import (
+	"archive/tar"
+	"bytes"
+	"compress/gzip"
+	"embed"
+	"fmt"
+	"io"
+	"io/fs"
+	"log"
+	"os"
+	"path/filepath"
+	"time"
+
+	"tailscale.com/release/dist"
+)
+
+type target struct {
+	filenameArch    string
+	dsmMajorVersion int
+	goenv           map[string]string
+	packageCenter   bool
+}
+
+func (t *target) String() string {
+	return fmt.Sprintf("synology/dsm%d/%s", t.dsmMajorVersion, t.filenameArch)
+}
+
+func (t *target) Build(b *dist.Build) ([]string, error) {
+	inner, err := getSynologyBuilds(b).buildInnerPackage(b, t.dsmMajorVersion, t.goenv)
+	if err != nil {
+		return nil, err
+	}
+
+	out, err := t.buildSPK(b, inner)
+	if err != nil {
+		return nil, err
+	}
+
+	return []string{out}, nil
+}
+
+func (t *target) buildSPK(b *dist.Build, inner *innerPkg) (string, error) {
+	filename := fmt.Sprintf("tailscale-%s-%s-%d-dsm%d.spk", t.filenameArch, b.Version.Short, b.Version.Synology[t.dsmMajorVersion], t.dsmMajorVersion)
+	out := filepath.Join(b.Out, filename)
+	log.Printf("Building %s", filename)
+
+	privFile := fmt.Sprintf("privilege-dsm%d", t.dsmMajorVersion)
+	if t.packageCenter && t.dsmMajorVersion == 7 {
+		privFile += ".for-package-center"
+	}
+
+	f, err := os.Create(out)
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+	tw := tar.NewWriter(f)
+	defer tw.Close()
+
+	err = writeTar(tw, b.Time,
+		memFile("INFO", t.mkInfo(b, inner.uncompressedSz), 0644),
+		static("PACKAGE_ICON.PNG", "PACKAGE_ICON.PNG", 0644),
+		static("PACKAGE_ICON_256.PNG", "PACKAGE_ICON_256.PNG", 0644),
+		static("Tailscale.sc", "Tailscale.sc", 0644),
+		dir("conf"),
+		static("resource", "conf/resource", 0644),
+		static(privFile, "conf/privilege", 0644),
+		file(inner.path, "package.tgz", 0644),
+		dir("scripts"),
+		static("scripts/start-stop-status", "scripts/start-stop-status", 0644),
+		static("scripts/postupgrade", "scripts/postupgrade", 0644),
+		static("scripts/preupgrade", "scripts/preupgrade", 0644),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	if err := tw.Close(); err != nil {
+		return "", err
+	}
+	if err := f.Close(); err != nil {
+		return "", err
+	}
+
+	return out, nil
+}
+
+func (t *target) mkInfo(b *dist.Build, uncompressedSz int64) []byte {
+	var ret bytes.Buffer
+	f := func(k, v string) {
+		fmt.Fprintf(&ret, "%s=%q\n", k, v)
+	}
+	f("package", "Tailscale")
+	f("version", fmt.Sprintf("%s-%d", b.Version.Short, b.Version.Synology[t.dsmMajorVersion]))
+	f("arch", t.filenameArch)
+	f("description", "Connect all your devices using WireGuard, without the hassle.")
+	f("displayname", "Tailscale")
+	f("maintainer", "Tailscale, Inc.")
+	f("maintainer_url", "https://github.com/tailscale/tailscale")
+	f("create_time", b.Time.Format("20060102-15:04:05"))
+	f("dsmuidir", "ui")
+	f("dsmappname", "SYNO.SDS.Tailscale")
+	f("startstop_restart_services", "nginx")
+	switch t.dsmMajorVersion {
+	case 6:
+		f("os_min_ver", "6.0.1-7445")
+		f("os_max_ver", "7.0-40000")
+	case 7:
+		f("os_min_ver", "7.0-40000")
+		f("os_max_ver", "")
+	default:
+		panic(fmt.Sprintf("unsupported DSM major version %d", t.dsmMajorVersion))
+	}
+	f("extractsize", fmt.Sprintf("%v", uncompressedSz>>10)) // in KiB
+	return ret.Bytes()
+}
+
+type synologyBuildsMemoizeKey struct{}
+
+type innerPkg struct {
+	path           string
+	uncompressedSz int64
+}
+
+// synologyBuilds is extra build context shared by all synology builds.
+type synologyBuilds struct {
+	innerPkgs dist.Memoize[*innerPkg]
+}
+
+// getSynologyBuilds returns the synologyBuilds for b, creating one if needed.
+func getSynologyBuilds(b *dist.Build) *synologyBuilds {
+	return b.Extra(synologyBuildsMemoizeKey{}, func() any { return new(synologyBuilds) }).(*synologyBuilds)
+}
+
+// buildInnerPackage builds the inner tarball for synology packages,
+// which contains the files to unpack to disk on installation (as
+// opposed to the outer tarball, which contains package metadata)
+func (m *synologyBuilds) buildInnerPackage(b *dist.Build, dsmVersion int, goenv map[string]string) (*innerPkg, error) {
+	key := []any{dsmVersion, goenv}
+	return m.innerPkgs.Do(key, func() (*innerPkg, error) {
+		ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", goenv)
+		if err != nil {
+			return nil, err
+		}
+		tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", goenv)
+		if err != nil {
+			return nil, err
+		}
+
+		tmp := b.TmpDir()
+		out := filepath.Join(tmp, "package.tgz")
+
+		f, err := os.Create(out)
+		if err != nil {
+			return nil, err
+		}
+		defer f.Close()
+		gw := gzip.NewWriter(f)
+		defer gw.Close()
+		cw := &countingWriter{gw, 0}
+		tw := tar.NewWriter(cw)
+		defer tw.Close()
+
+		err = writeTar(tw, b.Time,
+			dir("bin"),
+			file(tsd, "bin/tailscaled", 0755),
+			file(ts, "bin/tailscale", 0755),
+			dir("conf"),
+			static("Tailscale.sc", "conf/Tailscale.sc", 0644),
+			static(fmt.Sprintf("logrotate-dsm%d", dsmVersion), "conf/logrotate.conf", 0644),
+			dir("ui"),
+			static("PACKAGE_ICON_256.PNG", "ui/PACKAGE_ICON_256.PNG", 0644),
+			static("config", "ui/config", 0644), // TODO: this has "1.8.3" hard-coded in it; why? what is it? bug?
+			static("index.cgi", "ui/index.cgi", 0755))
+		if err != nil {
+			return nil, err
+		}
+
+		if err := tw.Close(); err != nil {
+			return nil, err
+		}
+		if err := gw.Close(); err != nil {
+			return nil, err
+		}
+		if err := f.Close(); err != nil {
+			return nil, err
+		}
+
+		return &innerPkg{out, cw.n}, nil
+	})
+}
+
+// writeTar writes ents to tw.
+func writeTar(tw *tar.Writer, modTime time.Time, ents ...tarEntry) error {
+	for _, ent := range ents {
+		if err := ent(tw, modTime); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// tarEntry is a function that writes tar entries (files or
+// directories) to a tar.Writer.
+type tarEntry func(*tar.Writer, time.Time) error
+
+// fsFile returns a tarEntry that writes src in fsys to dst in the tar
+// file, with mode.
+func fsFile(fsys fs.FS, src, dst string, mode int64) tarEntry {
+	return func(tw *tar.Writer, modTime time.Time) error {
+		f, err := fsys.Open(src)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+		fi, err := f.Stat()
+		if err != nil {
+			return err
+		}
+		hdr := &tar.Header{
+			Name:    dst,
+			Size:    fi.Size(),
+			Mode:    mode,
+			ModTime: modTime,
+		}
+		if err := tw.WriteHeader(hdr); err != nil {
+			return err
+		}
+		if _, err = io.Copy(tw, f); err != nil {
+			return err
+		}
+		return nil
+	}
+}
+
+// file returns a tarEntry that writes src on disk into the tar file as
+// dst, with mode.
+func file(src, dst string, mode int64) tarEntry {
+	return fsFile(os.DirFS(filepath.Dir(src)), filepath.Base(src), dst, mode)
+}
+
+//go:embed files/*
+var files embed.FS
+
+// static returns a tarEntry that writes src in files/ into the tar
+// file as dst, with mode.
+func static(src, dst string, mode int64) tarEntry {
+	fsys, err := fs.Sub(files, "files")
+	if err != nil {
+		panic(err)
+	}
+	return fsFile(fsys, src, dst, mode)
+}
+
+// memFile returns a tarEntry that writes bs to dst in the tar file,
+// with mode.
+func memFile(dst string, bs []byte, mode int64) tarEntry {
+	return func(tw *tar.Writer, modTime time.Time) error {
+		hdr := &tar.Header{
+			Name:    dst,
+			Size:    int64(len(bs)),
+			Mode:    mode,
+			ModTime: modTime,
+		}
+		if err := tw.WriteHeader(hdr); err != nil {
+			return err
+		}
+		if _, err := tw.Write(bs); err != nil {
+			return err
+		}
+		return nil
+	}
+}
+
+// dir returns a tarEntry that creates a world-readable directory in
+// the tar file.
+func dir(name string) tarEntry {
+	return func(tw *tar.Writer, modTime time.Time) error {
+		return tw.WriteHeader(&tar.Header{
+			Typeflag: tar.TypeDir,
+			Name:     name + "/",
+			Mode:     0755,
+			ModTime:  modTime,
+			// TODO: why tailscale? Files are being written as owned by root.
+			Uname: "tailscale",
+			Gname: "tailscale",
+		})
+	}
+}
+
+type countingWriter struct {
+	w io.Writer
+	n int64
+}
+
+func (cw *countingWriter) Write(bs []byte) (int, error) {
+	n, err := cw.w.Write(bs)
+	cw.n += int64(n)
+	return n, err
+}

+ 69 - 0
release/dist/synology/targets.go

@@ -0,0 +1,69 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package synology
+
+import "tailscale.com/release/dist"
+
+func Targets(forPackageCenter bool) []dist.Target {
+	var ret []dist.Target
+	for _, dsmVersion := range []int{6, 7} {
+		ret = append(ret,
+			&target{
+				filenameArch:    "x86_64",
+				dsmMajorVersion: dsmVersion,
+				goenv: map[string]string{
+					"GOOS":   "linux",
+					"GOARCH": "amd64",
+				},
+				packageCenter: forPackageCenter,
+			},
+			&target{
+				filenameArch:    "i686",
+				dsmMajorVersion: dsmVersion,
+				goenv: map[string]string{
+					"GOOS":   "linux",
+					"GOARCH": "386",
+				},
+				packageCenter: forPackageCenter,
+			},
+			&target{
+				filenameArch:    "armv8",
+				dsmMajorVersion: dsmVersion,
+				goenv: map[string]string{
+					"GOOS":   "linux",
+					"GOARCH": "arm64",
+				},
+				packageCenter: forPackageCenter,
+			})
+
+		// On older ARMv5 and ARMv7 platforms, synology used a whole
+		// mess of SoC-specific target names, even though the packages
+		// built for each are identical apart from metadata.
+		for _, v5Arch := range []string{"armv5", "88f6281", "88f6282"} {
+			ret = append(ret, &target{
+				filenameArch:    v5Arch,
+				dsmMajorVersion: dsmVersion,
+				goenv: map[string]string{
+					"GOOS":   "linux",
+					"GOARCH": "arm",
+					"GOARM":  "5",
+				},
+				packageCenter: forPackageCenter,
+			})
+		}
+		for _, v7Arch := range []string{"armv7", "alpine", "armada370", "armada375", "armada38x", "armadaxp", "comcerto2k", "monaco", "hi3535"} {
+			ret = append(ret, &target{
+				filenameArch:    v7Arch,
+				dsmMajorVersion: dsmVersion,
+				goenv: map[string]string{
+					"GOOS":   "linux",
+					"GOARCH": "arm",
+					"GOARM":  "7",
+				},
+				packageCenter: forPackageCenter,
+			})
+		}
+	}
+	return ret
+}

+ 2 - 4
release/dist/unixpkgs/pkgs.go

@@ -14,7 +14,6 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-	"time"
 
 	"github.com/goreleaser/nfpm"
 	"tailscale.com/release/dist"
@@ -71,7 +70,6 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) {
 	tw := tar.NewWriter(gw)
 	defer tw.Close()
 
-	buildTime := time.Now()
 	addFile := func(src, dst string, mode int64) error {
 		f, err := os.Open(src)
 		if err != nil {
@@ -86,7 +84,7 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) {
 			Name:    dst,
 			Size:    fi.Size(),
 			Mode:    mode,
-			ModTime: buildTime,
+			ModTime: b.Time,
 			Uid:     0,
 			Gid:     0,
 			Uname:   "root",
@@ -104,7 +102,7 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) {
 		hdr := &tar.Header{
 			Name:    name + "/",
 			Mode:    0755,
-			ModTime: buildTime,
+			ModTime: b.Time,
 			Uid:     0,
 			Gid:     0,
 			Uname:   "root",

+ 3 - 1
tool/gocross/autoflags.go

@@ -153,7 +153,9 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
 
 	env.Set("GOOS", targetOS)
 	env.Set("GOARCH", targetArch)
-	env.Set("GOARM", "5") // TODO: fix, see go/internal-bug/3092
+	if !env.IsSet("GOARM") {
+		env.Set("GOARM", "5") // TODO: fix, see go/internal-bug/3092
+	}
 	env.Set("GOMIPS", "softfloat")
 	env.Set("CGO_ENABLED", boolStr(cgo))
 	env.Set("CGO_CFLAGS", strings.Join(cgoCflags, " "))

+ 8 - 0
version/mkversion/mkversion.go

@@ -61,6 +61,10 @@ type VersionInfo struct {
 	// Winres is the version string that gets embedded into Windows exe
 	// metadata. It is of the form "x,y,z,0".
 	Winres string
+	// Synology is a map of Synology DSM major version to the
+	// Tailscale numeric version that gets embedded in Synology spk
+	// files.
+	Synology map[int]int64
 	// GitDate is the unix timestamp of GitHash's commit date.
 	GitDate string
 	// OtherDate is the unix timestamp of OtherHash's commit date, if any.
@@ -239,6 +243,10 @@ func mkOutput(v verInfo) (VersionInfo, error) {
 		GitHash: fmt.Sprintf("%s", v.hash),
 		GitDate: fmt.Sprintf("%s", v.date),
 		Track:   track,
+		Synology: map[int]int64{
+			6: 6*1_000_000_000 + int64(v.major-1)*1_000_000 + int64(v.minor)*1_000 + int64(v.patch),
+			7: 7*1_000_000_000 + int64(v.major-1)*1_000_000 + int64(v.minor)*1_000 + int64(v.patch),
+		},
 	}
 
 	if v.otherHash != "" {