mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-31 05:23:14 +00:00
clientupdate: parse /etc/synoinfo.conf to get CPU arch (#8940)
The hardware version in `/proc/sys/kernel/syno_hw_version` does not map exactly to versions in https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures. It contains some slightly different version formats. Instead, `/etc/synoinfo.conf` exists and contains a `unique` line with the CPU architecture encoded. Parse that out and filter through the list of architectures that we have SPKs for. Tested on DS218 and DS413j. Updates #8927 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
This commit is contained in:
parent
72d2122cad
commit
86ad1ea60e
@ -28,9 +28,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"tailscale.com/hostinfo"
|
|
||||||
"tailscale.com/net/tshttpproxy"
|
"tailscale.com/net/tshttpproxy"
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/must"
|
"tailscale.com/util/must"
|
||||||
"tailscale.com/util/winutil"
|
"tailscale.com/util/winutil"
|
||||||
@ -187,6 +185,8 @@ func (up *updater) confirm(ver string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const synoinfoConfPath = "/etc/synoinfo.conf"
|
||||||
|
|
||||||
func (up *updater) updateSynology() error {
|
func (up *updater) updateSynology() error {
|
||||||
if up.Version != "" {
|
if up.Version != "" {
|
||||||
return errors.New("installing a specific version on Synology is not supported")
|
return errors.New("installing a specific version on Synology is not supported")
|
||||||
@ -194,7 +194,7 @@ func (up *updater) updateSynology() error {
|
|||||||
|
|
||||||
// Get the latest version and list of SPKs from pkgs.tailscale.com.
|
// Get the latest version and list of SPKs from pkgs.tailscale.com.
|
||||||
osName := fmt.Sprintf("dsm%d", distro.DSMVersion())
|
osName := fmt.Sprintf("dsm%d", distro.DSMVersion())
|
||||||
arch, err := synoArch(hostinfo.New())
|
arch, err := synoArch(runtime.GOARCH, synoinfoConfPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -245,51 +245,62 @@ func (up *updater) updateSynology() error {
|
|||||||
|
|
||||||
// synoArch returns the Synology CPU architecture matching one of the SPK
|
// synoArch returns the Synology CPU architecture matching one of the SPK
|
||||||
// architectures served from pkgs.tailscale.com.
|
// architectures served from pkgs.tailscale.com.
|
||||||
func synoArch(hinfo *tailcfg.Hostinfo) (string, error) {
|
func synoArch(goArch, synoinfoPath string) (string, error) {
|
||||||
// Most Synology boxes just use a different arch name from GOARCH.
|
// Most Synology boxes just use a different arch name from GOARCH.
|
||||||
arch := map[string]string{
|
arch := map[string]string{
|
||||||
"amd64": "x86_64",
|
"amd64": "x86_64",
|
||||||
"386": "i686",
|
"386": "i686",
|
||||||
"arm64": "armv8",
|
"arm64": "armv8",
|
||||||
}[hinfo.GoArch]
|
}[goArch]
|
||||||
// Here's the fun part, some older ARM boxes require you to use SPKs
|
|
||||||
// specifically for their CPU.
|
|
||||||
//
|
|
||||||
// See https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures
|
|
||||||
// for a complete list. Here, we override GOARCH for those older boxes that
|
|
||||||
// support at least DSM6.
|
|
||||||
//
|
|
||||||
// This is an artisanal hand-crafted list based on the wiki page. Some
|
|
||||||
// values may be wrong, since we don't have all those devices to actually
|
|
||||||
// test with.
|
|
||||||
switch hinfo.DeviceModel {
|
|
||||||
case "DS213air", "DS213", "DS413j",
|
|
||||||
"DS112", "DS112+", "DS212", "DS212+", "RS212", "RS812", "DS212j", "DS112j",
|
|
||||||
"DS111", "DS211", "DS211+", "DS411slim", "DS411", "RS411", "DS211j", "DS411j":
|
|
||||||
arch = "88f6281"
|
|
||||||
case "NVR1218", "NVR216", "VS960HD", "VS360HD":
|
|
||||||
arch = "hi3535"
|
|
||||||
case "DS1517", "DS1817", "DS416", "DS2015xs", "DS715", "DS1515", "DS215+":
|
|
||||||
arch = "alpine"
|
|
||||||
case "DS216se", "DS115j", "DS114", "DS214se", "DS414slim", "RS214", "DS14", "EDS14", "DS213j":
|
|
||||||
arch = "armada370"
|
|
||||||
case "DS115", "DS215j":
|
|
||||||
arch = "armada375"
|
|
||||||
case "DS419slim", "DS218j", "RS217", "DS116", "DS216j", "DS216", "DS416slim", "RS816", "DS416j":
|
|
||||||
arch = "armada38x"
|
|
||||||
case "RS815", "DS214", "DS214+", "DS414", "RS814":
|
|
||||||
arch = "armadaxp"
|
|
||||||
case "DS414j":
|
|
||||||
arch = "comcerto2k"
|
|
||||||
case "DS216play":
|
|
||||||
arch = "monaco"
|
|
||||||
}
|
|
||||||
if arch == "" {
|
if arch == "" {
|
||||||
return "", fmt.Errorf("cannot determine CPU architecture for Synology model %q (Go arch %q), please report a bug at https://github.com/tailscale/tailscale/issues/new/choose", hinfo.DeviceModel, hinfo.GoArch)
|
// Here's the fun part, some older ARM boxes require you to use SPKs
|
||||||
|
// specifically for their CPU. See
|
||||||
|
// https://github.com/SynoCommunity/spksrc/wiki/Synology-and-SynoCommunity-Package-Architectures
|
||||||
|
// for a complete list.
|
||||||
|
//
|
||||||
|
// Some CPUs will map to neither this list nor the goArch map above, and we
|
||||||
|
// don't have SPKs for them.
|
||||||
|
cpu, err := parseSynoinfo(synoinfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get CPU architecture: %w", err)
|
||||||
|
}
|
||||||
|
switch cpu {
|
||||||
|
case "88f6281", "88f6282", "hi3535", "alpine", "armada370",
|
||||||
|
"armada375", "armada38x", "armadaxp", "comcerto2k", "monaco":
|
||||||
|
arch = cpu
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported Synology CPU architecture %q (Go arch %q), please report a bug at https://github.com/tailscale/tailscale/issues/new/choose", cpu, goArch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return arch, nil
|
return arch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSynoinfo(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Look for a line like:
|
||||||
|
// unique="synology_88f6282_413j"
|
||||||
|
// Extract the CPU in the middle (88f6282 in the above example).
|
||||||
|
s := bufio.NewScanner(f)
|
||||||
|
for s.Scan() {
|
||||||
|
l := s.Text()
|
||||||
|
if !strings.HasPrefix(l, "unique=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(l, "_", 3)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return "", fmt.Errorf(`malformed %q: found %q, expected format like 'unique="synology_$cpu_$model'`, path, l)
|
||||||
|
}
|
||||||
|
return parts[1], nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf(`missing "unique=" field in %q`, path)
|
||||||
|
}
|
||||||
|
|
||||||
func (up *updater) updateDebLike() error {
|
func (up *updater) updateDebLike() error {
|
||||||
ver, err := requestedTailscaleVersion(up.Version, up.track)
|
ver, err := requestedTailscaleVersion(up.Version, up.track)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -8,8 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"tailscale.com/tailcfg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
||||||
@ -446,29 +444,151 @@ tailscale installed size:
|
|||||||
|
|
||||||
func TestSynoArch(t *testing.T) {
|
func TestSynoArch(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
goarch string
|
goarch string
|
||||||
model string
|
synoinfoUnique string
|
||||||
want string
|
want string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{goarch: "amd64", model: "DS224+", want: "x86_64"},
|
{goarch: "amd64", synoinfoUnique: "synology_x86_224", want: "x86_64"},
|
||||||
{goarch: "arm64", model: "DS124", want: "armv8"},
|
{goarch: "arm64", synoinfoUnique: "synology_armv8_124", want: "armv8"},
|
||||||
{goarch: "386", model: "DS415play", want: "i686"},
|
{goarch: "386", synoinfoUnique: "synology_i686_415play", want: "i686"},
|
||||||
{goarch: "arm", model: "DS213air", want: "88f6281"},
|
{goarch: "arm", synoinfoUnique: "synology_88f6281_213air", want: "88f6281"},
|
||||||
{goarch: "arm", model: "NVR1218", want: "hi3535"},
|
{goarch: "arm", synoinfoUnique: "synology_88f6282_413j", want: "88f6282"},
|
||||||
{goarch: "arm", model: "DS1517", want: "alpine"},
|
{goarch: "arm", synoinfoUnique: "synology_hi3535_NVR1218", want: "hi3535"},
|
||||||
{goarch: "arm", model: "DS216se", want: "armada370"},
|
{goarch: "arm", synoinfoUnique: "synology_alpine_1517", want: "alpine"},
|
||||||
{goarch: "arm", model: "DS115", want: "armada375"},
|
{goarch: "arm", synoinfoUnique: "synology_armada370_216se", want: "armada370"},
|
||||||
{goarch: "arm", model: "DS419slim", want: "armada38x"},
|
{goarch: "arm", synoinfoUnique: "synology_armada375_115", want: "armada375"},
|
||||||
{goarch: "arm", model: "RS815", want: "armadaxp"},
|
{goarch: "arm", synoinfoUnique: "synology_armada38x_419slim", want: "armada38x"},
|
||||||
{goarch: "arm", model: "DS414j", want: "comcerto2k"},
|
{goarch: "arm", synoinfoUnique: "synology_armadaxp_RS815", want: "armadaxp"},
|
||||||
{goarch: "arm", model: "DS216play", want: "monaco"},
|
{goarch: "arm", synoinfoUnique: "synology_comcerto2k_414j", want: "comcerto2k"},
|
||||||
{goarch: "riscv64", model: "DS999", wantErr: true},
|
{goarch: "arm", synoinfoUnique: "synology_monaco_216play", want: "monaco"},
|
||||||
|
{goarch: "ppc64", synoinfoUnique: "synology_qoriq_413", wantErr: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.model), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.synoinfoUnique), func(t *testing.T) {
|
||||||
got, err := synoArch(&tailcfg.Hostinfo{GoArch: tt.goarch, DeviceModel: tt.model})
|
synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
|
||||||
|
if err := os.WriteFile(
|
||||||
|
synoinfoConfPath,
|
||||||
|
[]byte(fmt.Sprintf("unique=%q\n", tt.synoinfoUnique)),
|
||||||
|
0600,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := synoArch(tt.goarch, synoinfoConfPath)
|
||||||
|
if err != nil {
|
||||||
|
if !tt.wantErr {
|
||||||
|
t.Fatalf("got unexpected error %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
t.Fatalf("got %q, expected an error", got)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("got %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSynoinfo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
content string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "double-quoted",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
unique="synology_88f6281_213air"
|
||||||
|
`,
|
||||||
|
want: "88f6281",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "single-quoted",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
unique='synology_88f6281_213air'
|
||||||
|
`,
|
||||||
|
want: "88f6281",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unquoted",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
unique=synology_88f6281_213air
|
||||||
|
`,
|
||||||
|
want: "88f6281",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing unique",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty unique",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
unique=
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty unique double-quoted",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
unique=""
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty unique single-quoted",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
unique=''
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "malformed unique",
|
||||||
|
content: `
|
||||||
|
company_title="Synology"
|
||||||
|
unique="synology_88f6281"
|
||||||
|
`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty file",
|
||||||
|
content: ``,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty lines and comments",
|
||||||
|
content: `
|
||||||
|
|
||||||
|
# In a file named synoinfo? Shocking!
|
||||||
|
company_title="Synology"
|
||||||
|
|
||||||
|
|
||||||
|
# unique= is_a_field_that_follows
|
||||||
|
unique="synology_88f6281_213air"
|
||||||
|
|
||||||
|
`,
|
||||||
|
want: "88f6281",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
|
||||||
|
if err := os.WriteFile(synoinfoConfPath, []byte(tt.content), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := parseSynoinfo(synoinfoConfPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !tt.wantErr {
|
if !tt.wantErr {
|
||||||
t.Fatalf("got unexpected error %v", err)
|
t.Fatalf("got unexpected error %v", err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user