From 3e1daab7043d1e2f7a172174a27e333e78bcf569 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 10 Nov 2021 08:09:29 -0800 Subject: [PATCH] hostinfo, control/controlclient: tell control when Ubuntu has disabled Tailscale's sources Fixes #3177 Updates #2500 Change-Id: Iff2a8e27ec7d36a1c210263d6218f20ebed37924 Signed-off-by: Brad Fitzpatrick --- control/controlclient/direct.go | 3 ++ hostinfo/hostinfo.go | 55 +++++++++++++++++++++++++++++++++ hostinfo/hostinfo_test.go | 23 ++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 0f3178159..767c3e4ac 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -622,6 +622,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm if health.NetworkCategoryHealth() != nil { extraDebugFlags = append(extraDebugFlags, "warn-network-category-unhealthy") } + if hostinfo.DisabledEtcAptSource() { + extraDebugFlags = append(extraDebugFlags, "warn-etc-apt-source-disabled") + } if len(extraDebugFlags) > 0 { old := request.DebugFlags request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...) diff --git a/hostinfo/hostinfo.go b/hostinfo/hostinfo.go index 160728001..6b3f8e64c 100644 --- a/hostinfo/hostinfo.go +++ b/hostinfo/hostinfo.go @@ -7,11 +7,14 @@ package hostinfo import ( + "bufio" "io" "os" "path/filepath" "runtime" + "strings" "sync/atomic" + "time" "go4.org/mem" "tailscale.com/tailcfg" @@ -224,3 +227,55 @@ func inKubernetes() bool { } return false } + +type etcAptSrcResult struct { + mod time.Time + disabled bool +} + +var etcAptSrcCache atomic.Value // of etcAptSrcResult + +// DisabledEtcAptSource reports whether Ubuntu (or similar) has disabled +// the /etc/apt/sources.list.d/tailscale.list file contents upon upgrade +// to a new release of the distro. +// +// See https://github.com/tailscale/tailscale/issues/3177 +func DisabledEtcAptSource() bool { + if runtime.GOOS != "linux" { + return false + } + const path = "/etc/apt/sources.list.d/tailscale.list" + fi, err := os.Stat(path) + if err != nil || !fi.Mode().IsRegular() { + return false + } + mod := fi.ModTime() + if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod == mod { + return c.disabled + } + f, err := os.Open(path) + if err != nil { + return false + } + defer f.Close() + v := etcAptSourceFileIsDisabled(f) + etcAptSrcCache.Store(etcAptSrcResult{mod: mod, disabled: v}) + return v +} + +func etcAptSourceFileIsDisabled(r io.Reader) bool { + bs := bufio.NewScanner(r) + disabled := false // did we find the "disabled on upgrade" comment? + for bs.Scan() { + line := strings.TrimSpace(bs.Text()) + if strings.Contains(line, "# disabled on upgrade") { + disabled = true + } + if line == "" || line[0] == '#' { + continue + } + // Well, it has some contents in it at least. + return false + } + return disabled +} diff --git a/hostinfo/hostinfo_test.go b/hostinfo/hostinfo_test.go index 7346b78fe..8ec232d43 100644 --- a/hostinfo/hostinfo_test.go +++ b/hostinfo/hostinfo_test.go @@ -6,6 +6,7 @@ package hostinfo import ( "encoding/json" + "strings" "testing" ) @@ -27,3 +28,25 @@ func TestOSVersion(t *testing.T) { } t.Logf("Got: %#q", osVersion()) } + +func TestEtcAptSourceFileIsDisabled(t *testing.T) { + tests := []struct { + name string + in string + want bool + }{ + {"empty", "", false}, + {"normal", "deb foo\n", false}, + {"normal-commented", "# deb foo\n", false}, + {"normal-disabled-by-ubuntu", "# deb foo # disabled on upgrade to dingus\n", true}, + {"normal-disabled-then-uncommented", "deb foo # disabled on upgrade to dingus\n", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := etcAptSourceFileIsDisabled(strings.NewReader(tt.in)) + if got != tt.want { + t.Errorf("got %v; want %v", got, tt.want) + } + }) + } +}