From bb93e29d5c76f61c607e511f374596a3aba0c3ce Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 17 Feb 2022 15:00:41 -0800 Subject: [PATCH] tailcfg, ipn/ipnlocal: add Hostinfo.SSH_HostKeys, send when SSH enabled (The name SSH_HostKeys is bad but SSHHostKeys is worse.) Updates #3802 Change-Id: I2a889019c9e8b065b668dd58140db4fcab868a91 Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/local.go | 16 +++++++++++++--- ipn/ipnlocal/ssh.go | 16 +++++++++++++--- ipn/ipnlocal/ssh_stub.go | 12 ++++++++++++ ssh/tailssh/tailssh.go | 2 +- tailcfg/tailcfg.go | 5 +++++ tailcfg/tailcfg_clone.go | 2 ++ tailcfg/tailcfg_test.go | 7 ++++++- 7 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 ipn/ipnlocal/ssh_stub.go diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 75b3f6149..cdfcbce61 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -894,7 +894,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error { if b.inServerMode || runtime.GOOS == "windows" { b.logf("Start: serverMode=%v", b.inServerMode) } - applyPrefsToHostinfo(hostinfo, b.prefs) + b.applyPrefsToHostinfo(hostinfo, b.prefs) b.setNetMapLocked(nil) persistv := b.prefs.Persist @@ -1739,7 +1739,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) { oldHi := b.hostinfo newHi := oldHi.Clone() - applyPrefsToHostinfo(newHi, newp) + b.applyPrefsToHostinfo(newHi, newp) b.hostinfo = newHi hostInfoChanged := !oldHi.Equal(newHi) userID := b.userID @@ -2444,13 +2444,23 @@ func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) { return ret } -func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) { +// Warning: b.mu might be held. Currently (2022-02-17) both callers hold it. +func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) { if h := prefs.Hostname; h != "" { hi.Hostname = h } hi.RoutableIPs = append(prefs.AdvertiseRoutes[:0:0], prefs.AdvertiseRoutes...) hi.RequestTags = append(prefs.AdvertiseTags[:0:0], prefs.AdvertiseTags...) hi.ShieldsUp = prefs.ShieldsUp + + var sshHostKeys []string + if prefs.RunSSH { + // TODO(bradfitz): this is called with b.mu held. Not ideal. + // If the filesystem gets wedged or something we could block for + // a long time. But probably fine. + sshHostKeys = b.getSSHHostKeyPublicStrings() + } + hi.SSH_HostKeys = sshHostKeys } // enterState transitions the backend into newState, updating internal diff --git a/ipn/ipnlocal/ssh.go b/ipn/ipnlocal/ssh.go index ff81e6f1b..8cb5dafbe 100644 --- a/ipn/ipnlocal/ssh.go +++ b/ipn/ipnlocal/ssh.go @@ -11,6 +11,7 @@ import ( "errors" "io/ioutil" "os" + "strings" "golang.org/x/crypto/ssh" "tailscale.com/envknob" @@ -18,15 +19,16 @@ import ( var useHostKeys = envknob.Bool("TS_USE_SYSTEM_SSH_HOST_KEYS") -func (b *LocalBackend) GetSSHHostKeys() ([]ssh.Signer, error) { +func (b *LocalBackend) GetSSH_HostKeys() ([]ssh.Signer, error) { // TODO(bradfitz): generate host keys, at least as needed if // an existing SSH server didn't put them on disk. But also // because people may want tailscale-specific ones. For now be // lazy and reuse the host ones. - return b.getSystemSSHHostKeys() + return b.getSystemSSH_HostKeys() } -func (b *LocalBackend) getSystemSSHHostKeys() (ret []ssh.Signer, err error) { +func (b *LocalBackend) getSystemSSH_HostKeys() (ret []ssh.Signer, err error) { + // TODO(bradfitz): cache this? for _, typ := range []string{"rsa", "ecdsa", "ed25519"} { hostKey, err := ioutil.ReadFile("/etc/ssh/ssh_host_" + typ + "_key") if os.IsNotExist(err) { @@ -46,3 +48,11 @@ func (b *LocalBackend) getSystemSSHHostKeys() (ret []ssh.Signer, err error) { } return ret, nil } + +func (b *LocalBackend) getSSHHostKeyPublicStrings() (ret []string) { + signers, _ := b.GetSSH_HostKeys() + for _, signer := range signers { + ret = append(ret, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))) + } + return ret +} diff --git a/ipn/ipnlocal/ssh_stub.go b/ipn/ipnlocal/ssh_stub.go new file mode 100644 index 000000000..e1e56b726 --- /dev/null +++ b/ipn/ipnlocal/ssh_stub.go @@ -0,0 +1,12 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !linux +// +build !linux + +package ipnlocal + +func (b *LocalBackend) getSSHHostKeyPublicStrings() []string { + return nil +} diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go index 28c0f9f6b..91bd0dcc6 100644 --- a/ssh/tailssh/tailssh.go +++ b/ssh/tailssh/tailssh.go @@ -49,7 +49,7 @@ func Handle(logf logger.Logf, lb *ipnlocal.LocalBackend, c net.Conn) error { for k, v := range ssh.DefaultSubsystemHandlers { srv.SubsystemHandlers[k] = v } - keys, err := lb.GetSSHHostKeys() + keys, err := lb.GetSSH_HostKeys() if err != nil { return err } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index b0969b085..457fef1ad 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -454,6 +454,7 @@ type Hostinfo struct { RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim Services []Service `json:",omitempty"` // services advertised by this machine NetInfo *NetInfo `json:",omitempty"` + SSH_HostKeys []string `json:"sshHostKeys,omitempty"` // if advertised // NOTE: any new fields containing pointers in this type // require changes to Hostinfo.Equal. @@ -516,6 +517,10 @@ func (v HostinfoView) RequestTags() views.StringSlice { return views.StringSliceOf(v.ж.RequestTags) } +func (v HostinfoView) SSH_HostKeys() views.StringSlice { + return views.StringSliceOf(v.ж.SSH_HostKeys) +} + func (v HostinfoView) Services() ServiceSlice { return ServiceSliceOf(v.ж.Services) } diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index d3e3c7f28..d47fbed67 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -106,6 +106,7 @@ func (src *Hostinfo) Clone() *Hostinfo { dst.RequestTags = append(src.RequestTags[:0:0], src.RequestTags...) dst.Services = append(src.Services[:0:0], src.Services...) dst.NetInfo = src.NetInfo.Clone() + dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...) return dst } @@ -126,6 +127,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct { RequestTags []string Services []Service NetInfo *NetInfo + SSH_HostKeys []string }{}) // Clone makes a deep copy of NetInfo. diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index a75de335b..6d580064b 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -32,7 +32,7 @@ func TestHostinfoEqual(t *testing.T) { "ShieldsUp", "ShareeNode", "GoArch", "RoutableIPs", "RequestTags", - "Services", "NetInfo", + "Services", "NetInfo", "SSH_HostKeys", } if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) { t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n", @@ -181,6 +181,11 @@ func TestHostinfoEqual(t *testing.T) { &Hostinfo{}, false, }, + { + &Hostinfo{SSH_HostKeys: []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO.... root@bar"}}, + &Hostinfo{}, + false, + }, } for i, tt := range tests { got := tt.a.Equal(tt.b)