cmd/tailscale: generate known_hosts file for 'tailscale ssh'
Updates
package cli
import (
+ "bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
+ "path/filepath"
"runtime"
"strings"
"syscall"
"github.com/alessio/shellescape"
"github.com/peterbourgon/ff/v3/ffcli"
+ "tailscale.com/client/tailscale"
"tailscale.com/envknob"
+ "tailscale.com/ipn/ipnstate"
)
var sshCmd = &ffcli.Command{
@@ -52,9 +56,21 @@ func runSSH(ctx context.Context, args []string) error {
if err != nil {
return err
}
+ st, err := tailscale.Status(ctx)
+ if err != nil {
+ return err
+ }
+ knownHostsFile, err := writeKnownHosts(st)
+ if err != nil {
+ return err
+ }
+
argv := append([]string{
ssh,
+ "-o", fmt.Sprintf("UserKnownHostsFile %s",
+ shellescape.Quote(knownHostsFile),
+ ),
"-o", fmt.Sprintf("ProxyCommand %s --socket=%s nc %%h %%p",
shellescape.Quote(tailscaleBin),
shellescape.Quote(rootArgs.socket),
@@ -95,3 +111,52 @@ func runSSH(ctx context.Context, args []string) error {
}
return errors.New("unreachable")
}
+
+func writeKnownHosts(st *ipnstate.Status) (knownHostsFile string, err error) {
+ confDir, err := os.UserConfigDir()
+ if err != nil {
+ return "", err
+ }
+ tsConfDir := filepath.Join(confDir, "tailscale")
+ if err := os.MkdirAll(tsConfDir, 0700); err != nil {
+ return "", err
+ }
+ knownHostsFile = filepath.Join(tsConfDir, "ssh_known_hosts")
+ want := genKnownHosts(st)
+ if cur, err := os.ReadFile(knownHostsFile); err != nil || !bytes.Equal(cur, want) {
+ if err := os.WriteFile(knownHostsFile, want, 0644); err != nil {
+ return "", err
+ }
+ }
+ return knownHostsFile, nil
+}
+
+func genKnownHosts(st *ipnstate.Status) []byte {
+ var buf bytes.Buffer
+ for _, k := range st.Peers() {
+ ps := st.Peer[k]
+ if len(ps.SSH_HostKeys) == 0 {
+ continue
+ }
+ // addEntries adds one line per each of p's host keys.
+ addEntries := func(host string) {
+ for _, hk := range ps.SSH_HostKeys {
+ hostKey := strings.TrimSpace(hk)
+ if strings.ContainsAny(hostKey, "\n\r") { // invalid
+ continue
+ }
+ fmt.Fprintf(&buf, "%s %s\n", host, hostKey)
+ }
+ }
+ if ps.DNSName != "" {
+ addEntries(ps.DNSName)
+ }
+ if base, _, ok := strings.Cut(ps.DNSName, "."); ok {
+ addEntries(base)
+ }
+ for _, ip := range st.TailscaleIPs {
+ addEntries(ip.String())
+ }
+ }
+ return buf.Bytes()
+}
