cmd/tailscale/cli, ipn/ipnlocal: give SSH tips when off/unconfigured

Updates #3802

Change-Id: I6b9a3175f68a6daa670f912561f2c2ececc07770
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 467eb2eca0)
This commit is contained in:
Brad Fitzpatrick 2022-06-17 12:09:23 -07:00
parent c1795c6af9
commit 32f26a723b
2 changed files with 104 additions and 24 deletions

View File

@ -404,7 +404,7 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
return simpleUp, justEditMP, nil return simpleUp, justEditMP, nil
} }
func runUp(ctx context.Context, args []string) error { func runUp(ctx context.Context, args []string) (retErr error) {
if len(args) > 0 { if len(args) > 0 {
fatalf("too many non-flag arguments: %q", args) fatalf("too many non-flag arguments: %q", args)
} }
@ -481,6 +481,12 @@ func runUp(ctx context.Context, args []string) error {
} }
} }
defer func() {
if retErr == nil {
checkSSHUpWarnings(ctx)
}
}()
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env) simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
if err != nil { if err != nil {
fatalf("%s", err) fatalf("%s", err)
@ -676,6 +682,28 @@ func runUp(ctx context.Context, args []string) error {
} }
} }
func checkSSHUpWarnings(ctx context.Context) {
if !upArgs.runSSH {
return
}
st, err := localClient.Status(ctx)
if err != nil {
// Ignore. Don't spam more.
return
}
if len(st.Health) == 0 {
return
}
if len(st.Health) == 1 && strings.Contains(st.Health[0], "SSH") {
printf("%s\n", st.Health[0])
return
}
printf("# Health check:\n")
for _, m := range st.Health {
printf(" - %s\n", m)
}
}
func printUpDoneJSON(state ipn.State, errorString string) { func printUpDoneJSON(state ipn.State, errorString string) {
js := &upOutputJSON{BackendState: state.String(), Error: errorString} js := &upOutputJSON{BackendState: state.String(), Error: errorString}
data, err := json.MarshalIndent(js, "", " ") data, err := json.MarshalIndent(js, "", " ")

View File

@ -416,6 +416,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
s.Health = append(s.Health, err.Error()) s.Health = append(s.Health, err.Error())
} }
} }
if m := b.sshOnButUnusableHealthCheckMessageLocked(); m != "" {
s.Health = append(s.Health, m)
}
if b.netMap != nil { if b.netMap != nil {
s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...) s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...)
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix() s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
@ -1826,39 +1829,88 @@ func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
} }
func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error { func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
var errs []error
if p.Hostname == "badhostname.tailscale." { if p.Hostname == "badhostname.tailscale." {
// Keep this one just for testing. // Keep this one just for testing.
return errors.New("bad hostname [test]") errs = append(errs, errors.New("bad hostname [test]"))
} }
if p.RunSSH { if err := b.checkSSHPrefsLocked(p); err != nil {
switch runtime.GOOS { errs = append(errs, err)
case "linux": }
if distro.Get() == distro.Synology && !envknob.UseWIPCode() { return multierr.New(errs...)
return errors.New("The Tailscale SSH server does not run on Synology.") }
}
// otherwise okay func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
case "darwin": if !p.RunSSH {
// okay only in tailscaled mode for now. return nil
if version.IsSandboxedMacOS() { }
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.") switch runtime.GOOS {
} case "linux":
if !envknob.UseWIPCode() { if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1") return errors.New("The Tailscale SSH server does not run on Synology.")
}
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
} }
if !canSSH { // otherwise okay
return errors.New("The Tailscale SSH server has been administratively disabled.") case "darwin":
// okay only in tailscaled mode for now.
if version.IsSandboxedMacOS() {
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
} }
if b.netMap != nil && b.netMap.SSHPolicy == nil && if !envknob.UseWIPCode() {
envknob.SSHPolicyFile() == "" && !envknob.SSHIgnoreTailnetPolicy() { return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
return errors.New("Unable to enable local Tailscale SSH server; not enabled/configured on Tailnet.") }
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}
if !canSSH {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
return nil
}
if b.netMap != nil {
if !hasCapability(b.netMap, tailcfg.CapabilitySSH) {
if b.isDefaultServerLocked() {
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet. See https://tailscale.com/s/ssh")
}
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet.")
} }
} }
return nil return nil
} }
func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage string) {
if b.prefs == nil || !b.prefs.RunSSH {
return ""
}
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
return "development SSH policy in use"
}
nm := b.netMap
if nm == nil {
return ""
}
if nm.SSHPolicy != nil && len(nm.SSHPolicy.Rules) > 0 {
return ""
}
isDefault := b.isDefaultServerLocked()
isAdmin := hasCapability(nm, tailcfg.CapabilityAdmin)
if !isAdmin {
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access."
}
if !isDefault {
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs to allow access."
}
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs at https://tailscale.com/s/ssh-policy"
}
func (b *LocalBackend) isDefaultServerLocked() bool {
if b.prefs == nil {
return true // assume true until set otherwise
}
return b.prefs.ControlURLOrDefault() == ipn.DefaultControlURL
}
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) { func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
b.mu.Lock() b.mu.Lock()
p0 := b.prefs.Clone() p0 := b.prefs.Clone()