mirror of
https://github.com/tailscale/tailscale.git
synced 2026-03-27 02:31:30 -04:00
clientupdate,net/tstun: add support for OpenWrt 25.12.0 using apk (#18545)
OpenWrt is changing to using alpine like `apk` for package installation over its previous opkg. Additionally, they are not using the same repo files as alpine making installation fail. Add support for the new repository files and ensure that the required package detection system uses apk. Updates #18535 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
This commit is contained in:
@@ -661,7 +661,7 @@ func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
|
||||
|
||||
func (up *Updater) updateAlpineLike() (err error) {
|
||||
if up.Version != "" {
|
||||
return errors.New("installing a specific version on Alpine-based distros is not supported")
|
||||
return errors.New("installing a specific version on apk-based distros is not supported")
|
||||
}
|
||||
if err := requireRoot(); err != nil {
|
||||
return err
|
||||
@@ -691,7 +691,7 @@ func (up *Updater) updateAlpineLike() (err error) {
|
||||
return fmt.Errorf(`failed to parse latest version from "apk info tailscale": %w`, err)
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
if err := checkOutdatedAlpineRepo(up.Logf, ver, up.Track); err != nil {
|
||||
if err := checkOutdatedAlpineRepo(up.Logf, apkDirPaths, ver, up.Track); err != nil {
|
||||
up.Logf("failed to check whether Alpine release is outdated: %v", err)
|
||||
}
|
||||
return nil
|
||||
@@ -731,9 +731,12 @@ func parseAlpinePackageVersion(out []byte) (string, error) {
|
||||
return "", errors.New("tailscale version not found in output")
|
||||
}
|
||||
|
||||
var apkRepoVersionRE = regexp.MustCompile(`v[0-9]+\.[0-9]+`)
|
||||
var (
|
||||
apkRepoVersionRE = regexp.MustCompile(`v[0-9]+\.[0-9]+`)
|
||||
apkDirPaths = []string{"/etc/apk/repositories", "/etc/apk/repositories.d/distfeeds.list"}
|
||||
)
|
||||
|
||||
func checkOutdatedAlpineRepo(logf logger.Logf, apkVer, track string) error {
|
||||
func checkOutdatedAlpineRepo(logf logger.Logf, filePaths []string, apkVer, track string) error {
|
||||
latest, err := LatestTailscaleVersion(track)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -742,22 +745,34 @@ func checkOutdatedAlpineRepo(logf logger.Logf, apkVer, track string) error {
|
||||
// Actually on latest release.
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open("/etc/apk/repositories")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
// Read the first repo line. Typically, there are multiple repos that all
|
||||
// contain the same version in the path, like:
|
||||
// https://dl-cdn.alpinelinux.org/alpine/v3.20/main
|
||||
// https://dl-cdn.alpinelinux.org/alpine/v3.20/community
|
||||
s := bufio.NewScanner(f)
|
||||
if !s.Scan() {
|
||||
return s.Err()
|
||||
}
|
||||
alpineVer := apkRepoVersionRE.FindString(s.Text())
|
||||
if alpineVer != "" {
|
||||
logf("The latest Tailscale release for Linux is %q, but your apk repository only provides %q.\nYour Alpine version is %q, you may need to upgrade the system to get the latest Tailscale version: https://wiki.alpinelinux.org/wiki/Upgrading_Alpine", latest, apkVer, alpineVer)
|
||||
|
||||
// OpenWrt uses a different repo file in repositories.d, check for that as well.
|
||||
for _, repoFile := range filePaths {
|
||||
f, err := os.Open(repoFile)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
// Read the first repo line. Typically, there are multiple repos that all
|
||||
// contain the same version in the path, like:
|
||||
// https://dl-cdn.alpinelinux.org/alpine/v3.20/main
|
||||
// https://dl-cdn.alpinelinux.org/alpine/v3.20/community
|
||||
s := bufio.NewScanner(f)
|
||||
if !s.Scan() {
|
||||
if s.Err() != nil {
|
||||
return s.Err()
|
||||
}
|
||||
logf("The latest Tailscale release for Linux is %q, but your apk repository only provides %q.\nYou may need to upgrade your Alpine system to get the latest Tailscale version: https://wiki.alpinelinux.org/wiki/Upgrading_Alpine", latest, apkVer)
|
||||
}
|
||||
alpineVer := apkRepoVersionRE.FindString(s.Text())
|
||||
if alpineVer != "" {
|
||||
logf("The latest Tailscale release for Linux is %q, but your apk repository only provides %q.\nYour Alpine version is %q, you may need to upgrade the system to get the latest Tailscale version: https://wiki.alpinelinux.org/wiki/Upgrading_Alpine", latest, apkVer, alpineVer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1246,8 +1261,10 @@ type trackPackages struct {
|
||||
SPKsVersion string
|
||||
}
|
||||
|
||||
var tailscaleHTTPEndpoint = "https://pkgs.tailscale.com"
|
||||
|
||||
func latestPackages(track string) (*trackPackages, error) {
|
||||
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/?mode=json&os=%s", track, runtime.GOOS)
|
||||
url := fmt.Sprintf("%s/%s/?mode=json&os=%s", tailscaleHTTPEndpoint, track, runtime.GOOS)
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching latest tailscale version: %w", err)
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -299,6 +302,127 @@ func TestParseAlpinePackageVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckOutdatedAlpineRepo(t *testing.T) {
|
||||
anyToString := func(a any) string {
|
||||
str, ok := a.(string)
|
||||
if !ok {
|
||||
panic("failed to parse param as string")
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fileContent string
|
||||
latestHTTPVersion string
|
||||
latestApkVersion string
|
||||
wantHTTPVersion string
|
||||
wantApkVersion string
|
||||
wantAlpineVersion string
|
||||
track string
|
||||
}{
|
||||
{
|
||||
name: "Up to date",
|
||||
fileContent: "https://dl-cdn.alpinelinux.org/alpine/v3.20/main",
|
||||
latestHTTPVersion: "1.95.3",
|
||||
latestApkVersion: "1.95.3",
|
||||
track: "unstable",
|
||||
},
|
||||
{
|
||||
name: "Behind unstable",
|
||||
fileContent: "https://dl-cdn.alpinelinux.org/alpine/v3.20/main",
|
||||
latestHTTPVersion: "1.95.4",
|
||||
latestApkVersion: "1.95.3",
|
||||
wantHTTPVersion: "1.95.4",
|
||||
wantApkVersion: "1.95.3",
|
||||
wantAlpineVersion: "v3.20",
|
||||
track: "unstable",
|
||||
},
|
||||
{
|
||||
name: "Behind stable",
|
||||
fileContent: "https://dl-cdn.alpinelinux.org/alpine/v2.40/main",
|
||||
latestHTTPVersion: "1.94.3",
|
||||
latestApkVersion: "1.92.1",
|
||||
wantHTTPVersion: "1.94.3",
|
||||
wantApkVersion: "1.92.1",
|
||||
wantAlpineVersion: "v2.40",
|
||||
track: "stable",
|
||||
},
|
||||
{
|
||||
name: "Nothing in dist file",
|
||||
fileContent: "",
|
||||
latestHTTPVersion: "1.94.3",
|
||||
latestApkVersion: "1.92.1",
|
||||
wantHTTPVersion: "1.94.3",
|
||||
wantApkVersion: "1.92.1",
|
||||
track: "stable",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "example")
|
||||
if err != nil {
|
||||
t.Fatalf("error creating temp dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(dir) }) // clean up
|
||||
|
||||
file := filepath.Join(dir, "distfile")
|
||||
if err := os.WriteFile(file, []byte(tt.fileContent), 0o666); err != nil {
|
||||
t.Fatalf("error creating dist file: %v", err)
|
||||
}
|
||||
|
||||
testServ := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, _ *http.Request) {
|
||||
version := trackPackages{
|
||||
MSIsVersion: tt.latestHTTPVersion,
|
||||
MacZipsVersion: tt.latestHTTPVersion,
|
||||
TarballsVersion: tt.latestHTTPVersion,
|
||||
SPKsVersion: tt.latestHTTPVersion,
|
||||
}
|
||||
jsonData, err := json.Marshal(version)
|
||||
if err != nil {
|
||||
t.Errorf("failed to marshal version string: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := w.Write(jsonData); err != nil {
|
||||
t.Errorf("failed to write json blob: %v", err)
|
||||
}
|
||||
},
|
||||
))
|
||||
defer testServ.Close()
|
||||
|
||||
oldEndpoint := tailscaleHTTPEndpoint
|
||||
tailscaleHTTPEndpoint = testServ.URL
|
||||
defer func() { tailscaleHTTPEndpoint = oldEndpoint }()
|
||||
|
||||
var paramLatest string
|
||||
var paramApkVer string
|
||||
var paramAlpineVer string
|
||||
logf := func(_ string, params ...any) {
|
||||
paramLatest = anyToString(params[0])
|
||||
paramApkVer = anyToString(params[1])
|
||||
if len(params) > 2 {
|
||||
paramAlpineVer = anyToString(params[2])
|
||||
}
|
||||
}
|
||||
|
||||
err = checkOutdatedAlpineRepo(logf, []string{file}, tt.latestApkVersion, tt.track)
|
||||
if err != nil {
|
||||
t.Errorf("did not expect error, got: %v", err)
|
||||
}
|
||||
if paramLatest != tt.wantHTTPVersion {
|
||||
t.Errorf("expected HTTP version '%s', got '%s'", tt.wantHTTPVersion, paramLatest)
|
||||
}
|
||||
if paramApkVer != tt.wantApkVersion {
|
||||
t.Errorf("expected APK version '%s', got '%s'", tt.wantApkVersion, paramApkVer)
|
||||
}
|
||||
if paramAlpineVer != tt.wantAlpineVersion {
|
||||
t.Errorf("expected alpine version '%s', got '%s'", tt.wantAlpineVersion, paramAlpineVer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynoArch(t *testing.T) {
|
||||
tests := []struct {
|
||||
goarch string
|
||||
|
||||
@@ -86,14 +86,32 @@ func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf, createErr error)
|
||||
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut)
|
||||
}
|
||||
case distro.OpenWrt:
|
||||
out, err := exec.Command("opkg", "list-installed").CombinedOutput()
|
||||
if err != nil {
|
||||
logf("error querying OpenWrt installed packages: %s", out)
|
||||
return
|
||||
}
|
||||
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
|
||||
if !bytes.Contains(out, []byte(pkg+" - ")) {
|
||||
logf("Missing required package %s; run: opkg install %s", pkg, pkg)
|
||||
// OpenWRT switched to using apk as a package manager as of OpenWrt 25.12.0.
|
||||
// Find out what is used on this system and use that, Maybe we can get rid
|
||||
// of opkg in the future but for now keep checking.
|
||||
|
||||
if path, err := exec.LookPath("apk"); err == nil && path != "" {
|
||||
// Test with apk
|
||||
out, err := exec.Command("apk", "info").CombinedOutput()
|
||||
if err != nil {
|
||||
logf("error querying OpenWrt installed packages with apk: %s", out)
|
||||
return
|
||||
}
|
||||
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
|
||||
if !bytes.Contains(out, []byte(pkg)) {
|
||||
logf("Missing required package %s; run: apk add %s", pkg, pkg)
|
||||
}
|
||||
}
|
||||
} else { // Check for package with opkg (legacy)
|
||||
out, err := exec.Command("opkg", "list-installed").CombinedOutput()
|
||||
if err != nil {
|
||||
logf("error querying OpenWrt installed packages with opkg: %s", out)
|
||||
return
|
||||
}
|
||||
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
|
||||
if !bytes.Contains(out, []byte(pkg+" - ")) {
|
||||
logf("Missing required package %s; run: opkg install %s", pkg, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user