diff --git a/clientupdate/clientupdate.go b/clientupdate/clientupdate.go index 9e36cea21..b55623f8e 100644 --- a/clientupdate/clientupdate.go +++ b/clientupdate/clientupdate.go @@ -28,9 +28,7 @@ import ( "time" "github.com/google/uuid" - "tailscale.com/hostinfo" "tailscale.com/net/tshttpproxy" - "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/util/must" "tailscale.com/util/winutil" @@ -187,6 +185,8 @@ func (up *updater) confirm(ver string) bool { return true } +const synoinfoConfPath = "/etc/synoinfo.conf" + func (up *updater) updateSynology() error { if up.Version != "" { 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. osName := fmt.Sprintf("dsm%d", distro.DSMVersion()) - arch, err := synoArch(hostinfo.New()) + arch, err := synoArch(runtime.GOARCH, synoinfoConfPath) if err != nil { return err } @@ -245,51 +245,62 @@ func (up *updater) updateSynology() error { // synoArch returns the Synology CPU architecture matching one of the SPK // 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. arch := map[string]string{ "amd64": "x86_64", "386": "i686", "arm64": "armv8", - }[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. 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" - } + }[goArch] + 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 } +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 { ver, err := requestedTailscaleVersion(up.Version, up.track) if err != nil { diff --git a/clientupdate/clientupdate_test.go b/clientupdate/clientupdate_test.go index ec96ea79d..83aa6a07e 100644 --- a/clientupdate/clientupdate_test.go +++ b/clientupdate/clientupdate_test.go @@ -8,8 +8,6 @@ import ( "os" "path/filepath" "testing" - - "tailscale.com/tailcfg" ) func TestUpdateDebianAptSourcesListBytes(t *testing.T) { @@ -446,29 +444,151 @@ tailscale installed size: func TestSynoArch(t *testing.T) { tests := []struct { - goarch string - model string + goarch string + synoinfoUnique string + want string + wantErr bool + }{ + {goarch: "amd64", synoinfoUnique: "synology_x86_224", want: "x86_64"}, + {goarch: "arm64", synoinfoUnique: "synology_armv8_124", want: "armv8"}, + {goarch: "386", synoinfoUnique: "synology_i686_415play", want: "i686"}, + {goarch: "arm", synoinfoUnique: "synology_88f6281_213air", want: "88f6281"}, + {goarch: "arm", synoinfoUnique: "synology_88f6282_413j", want: "88f6282"}, + {goarch: "arm", synoinfoUnique: "synology_hi3535_NVR1218", want: "hi3535"}, + {goarch: "arm", synoinfoUnique: "synology_alpine_1517", want: "alpine"}, + {goarch: "arm", synoinfoUnique: "synology_armada370_216se", want: "armada370"}, + {goarch: "arm", synoinfoUnique: "synology_armada375_115", want: "armada375"}, + {goarch: "arm", synoinfoUnique: "synology_armada38x_419slim", want: "armada38x"}, + {goarch: "arm", synoinfoUnique: "synology_armadaxp_RS815", want: "armadaxp"}, + {goarch: "arm", synoinfoUnique: "synology_comcerto2k_414j", want: "comcerto2k"}, + {goarch: "arm", synoinfoUnique: "synology_monaco_216play", want: "monaco"}, + {goarch: "ppc64", synoinfoUnique: "synology_qoriq_413", wantErr: true}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.synoinfoUnique), func(t *testing.T) { + 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 }{ - {goarch: "amd64", model: "DS224+", want: "x86_64"}, - {goarch: "arm64", model: "DS124", want: "armv8"}, - {goarch: "386", model: "DS415play", want: "i686"}, - {goarch: "arm", model: "DS213air", want: "88f6281"}, - {goarch: "arm", model: "NVR1218", want: "hi3535"}, - {goarch: "arm", model: "DS1517", want: "alpine"}, - {goarch: "arm", model: "DS216se", want: "armada370"}, - {goarch: "arm", model: "DS115", want: "armada375"}, - {goarch: "arm", model: "DS419slim", want: "armada38x"}, - {goarch: "arm", model: "RS815", want: "armadaxp"}, - {goarch: "arm", model: "DS414j", want: "comcerto2k"}, - {goarch: "arm", model: "DS216play", want: "monaco"}, - {goarch: "riscv64", model: "DS999", wantErr: true}, - } + { + 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(fmt.Sprintf("%s-%s", tt.goarch, tt.model), func(t *testing.T) { - got, err := synoArch(&tailcfg.Hostinfo{GoArch: tt.goarch, DeviceModel: tt.model}) + 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 !tt.wantErr { t.Fatalf("got unexpected error %v", err)