ssh/tailssh, ipnlocal, controlclient: fetch next SSHAction from network

Updates #3802

Change-Id: I08e98805ab86d6bbabb6c365ed4526f54742fd8e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2022-03-10 10:28:42 -08:00
committed by Brad Fitzpatrick
parent 6b11004a2a
commit efc48b0578
7 changed files with 108 additions and 16 deletions

View File

@@ -15,6 +15,7 @@ import (
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"os/user"
@@ -26,6 +27,7 @@ import (
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/logtail/backoff"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
@@ -200,19 +202,40 @@ func (srv *server) handleSSH(s ssh.Session) {
s.Exit(1)
return
}
if action.Message != "" {
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
}
if action.Reject {
logf("ssh: access denied for %q from %v", ci.uprof.LoginName, ci.src.IP())
s.Exit(1)
return
}
if !action.Accept || action.HoldAndDelegate != "" {
fmt.Fprintf(s, "TODO: other SSHAction outcomes")
s.Exit(1)
return
// Loop processing/fetching Actions until one reaches a
// terminal state (Accept, Reject, or invalid Action), or
// until fetchSSHAction times out due to the context being
// done (client disconnect) or its 30 minute timeout passes.
// (Which is a long time for somebody to see login
// instructions and go to a URL to do something.)
ProcessAction:
for {
if action.Message != "" {
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
}
if action.Reject {
logf("ssh: access denied for %q from %v", ci.uprof.LoginName, ci.src.IP())
s.Exit(1)
return
}
if action.Accept {
break ProcessAction
}
url := action.HoldAndDelegate
if url == "" {
logf("ssh: access denied; SSHAction has neither Reject, Accept, or next step URL")
s.Exit(1)
return
}
action, err = srv.fetchSSHAction(s.Context(), url)
if err != nil {
logf("ssh: fetching SSAction from %s: %v", url, err)
s.Exit(1)
return
}
}
lu, err := user.Lookup(localUser)
if err != nil {
logf("ssh: user Lookup %q: %v", localUser, err)
@@ -235,6 +258,37 @@ func (srv *server) handleSSH(s ssh.Session) {
srv.handleAcceptedSSH(ctx, s, ci, lu)
}
func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
defer cancel()
bo := backoff.NewBackoff("fetch-ssh-action", srv.logf, 10*time.Second)
for {
if err := ctx.Err(); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
res, err := srv.lb.DoNoiseRequest(req)
if err != nil {
bo.BackOff(ctx, err)
continue
}
if res.StatusCode != 200 {
res.Body.Close()
bo.BackOff(ctx, fmt.Errorf("unexpected status: %v", res.Status))
continue
}
a := new(tailcfg.SSHAction)
if err := json.NewDecoder(res.Body).Decode(a); err != nil {
bo.BackOff(ctx, err)
continue
}
return a, nil
}
}
func (srv *server) handleSessionTermination(ctx context.Context, s ssh.Session, ci *sshConnInfo, cmd *exec.Cmd, exitOnce *sync.Once) {
<-ctx.Done()
// Either the process has already existed, in which case this does nothing.