|
|
@@ -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.
|