tailscale/ssh/tailssh/tailssh.go
Brad Fitzpatrick fbff1555fc ipnlocal, tailssh: start moving host key stuff into the right spot
Make tailssh ask LocalBackend for the SSH hostkeys, as we'll need to
distribute them to peers.

For now only the hacky use-same-as-actual-host mode is implemented.

Updates #3802

Change-Id: I819dcb25c14e42e6692c441186c1dc744441592b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-02-17 14:01:50 -08:00

153 lines
3.5 KiB
Go

// Copyright (c) 2021 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 tailssh is an SSH server integrated into Tailscale.
package tailssh
import (
"encoding/json"
"fmt"
"io"
"net"
"os"
"os/exec"
"syscall"
"unsafe"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
)
// TODO(bradfitz): this is all very temporary as code is temporarily
// being moved around; it will be restructured and documented in
// following commits.
// Handle handles an SSH connection from c.
func Handle(logf logger.Logf, lb *ipnlocal.LocalBackend, c net.Conn) error {
sshd := &server{lb, logf}
srv := &ssh.Server{
Handler: sshd.handleSSH,
RequestHandlers: map[string]ssh.RequestHandler{},
SubsystemHandlers: map[string]ssh.SubsystemHandler{},
ChannelHandlers: map[string]ssh.ChannelHandler{},
}
for k, v := range ssh.DefaultRequestHandlers {
srv.RequestHandlers[k] = v
}
for k, v := range ssh.DefaultChannelHandlers {
srv.ChannelHandlers[k] = v
}
for k, v := range ssh.DefaultSubsystemHandlers {
srv.SubsystemHandlers[k] = v
}
keys, err := lb.GetSSHHostKeys()
if err != nil {
return err
}
for _, signer := range keys {
srv.AddHostKey(signer)
}
srv.HandleConn(c)
return nil
}
type server struct {
lb *ipnlocal.LocalBackend
logf logger.Logf
}
func (srv *server) handleSSH(s ssh.Session) {
lb := srv.lb
logf := srv.logf
user := s.User()
addr := s.RemoteAddr()
logf("Handling SSH from %v for user %v", addr, user)
ta, ok := addr.(*net.TCPAddr)
if !ok {
logf("tsshd: rejecting non-TCP addr %T %v", addr, addr)
s.Exit(1)
return
}
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
if !ok {
logf("tsshd: rejecting unparseable addr %v", ta.IP)
s.Exit(1)
return
}
if !tsaddr.IsTailscaleIP(tanetaddr) {
logf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
s.Exit(1)
return
}
ptyReq, winCh, isPty := s.Pty()
if !isPty {
fmt.Fprintf(s, "TODO scp etc")
s.Exit(1)
return
}
srcIPP := netaddr.IPPortFrom(tanetaddr, uint16(ta.Port))
node, uprof, ok := lb.WhoIs(srcIPP)
if !ok {
fmt.Fprintf(s, "Hello, %v. I don't know who you are.\n", srcIPP)
s.Exit(0)
return
}
allow := envknob.String("TS_SSH_ALLOW_LOGIN")
if allow == "" || uprof.LoginName != allow {
logf("ssh: access denied for %q (only allowing %q)", uprof.LoginName, allow)
jnode, _ := json.Marshal(node)
jprof, _ := json.Marshal(uprof)
fmt.Fprintf(s, "Access denied.\n\nYou are node: %s\n\nYour profile: %s\n\nYou wanted %+v\n", jnode, jprof, ptyReq)
s.Exit(1)
return
}
var cmd *exec.Cmd
sshUser := s.User()
if os.Getuid() != 0 || sshUser == "root" {
cmd = exec.Command("/bin/bash")
} else {
cmd = exec.Command("/usr/bin/env", "su", "-", sshUser)
}
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
f, err := pty.Start(cmd)
if err != nil {
logf("running shell: %v", err)
s.Exit(1)
return
}
defer f.Close()
go func() {
for win := range winCh {
setWinsize(f, win.Width, win.Height)
}
}()
go func() {
io.Copy(f, s) // stdin
}()
io.Copy(s, f) // stdout
cmd.Process.Kill()
if err := cmd.Wait(); err != nil {
s.Exit(1)
}
s.Exit(0)
return
}
func setWinsize(f *os.File, w, h int) {
syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
}