mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
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:
parent
6b11004a2a
commit
efc48b0578
@ -7,6 +7,7 @@ package controlclient
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -725,3 +726,7 @@ func (c *Auto) TestOnlyTimeNow() time.Time {
|
|||||||
func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
func (c *Auto) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||||
return c.direct.SetDNS(ctx, req)
|
return c.direct.SetDNS(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Auto) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||||
|
return c.direct.DoNoiseRequest(req)
|
||||||
|
}
|
||||||
|
@ -11,6 +11,7 @@ package controlclient
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@ -82,6 +83,9 @@ type Client interface {
|
|||||||
// SetDNS sends the SetDNSRequest request to the control plane server,
|
// SetDNS sends the SetDNSRequest request to the control plane server,
|
||||||
// requesting a DNS record be created or updated.
|
// requesting a DNS record be created or updated.
|
||||||
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
|
SetDNS(context.Context, *tailcfg.SetDNSRequest) error
|
||||||
|
// DoNoiseRequest sends an HTTP request to the control plane
|
||||||
|
// over the Noise transport.
|
||||||
|
DoNoiseRequest(*http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserVisibleError is an error that should be shown to users.
|
// UserVisibleError is an error that should be shown to users.
|
||||||
|
@ -1428,6 +1428,14 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||||
|
nc, err := c.getNoiseClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nc.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
|
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
|
||||||
// with ping response data.
|
// with ping response data.
|
||||||
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
|
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
|
||||||
|
@ -3241,3 +3241,15 @@ func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
|||||||
}
|
}
|
||||||
return mc, nil
|
return mc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DoNoiseRequest sends a request to URL over the the control plane
|
||||||
|
// Noise connection.
|
||||||
|
func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
cc := b.cc
|
||||||
|
b.mu.Unlock()
|
||||||
|
if cc == nil {
|
||||||
|
return nil, errors.New("no client")
|
||||||
|
}
|
||||||
|
return cc.DoNoiseRequest(req)
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ package ipnlocal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -266,6 +267,10 @@ func (*mockControl) SetDNS(context.Context, *tailcfg.SetDNSRequest) error {
|
|||||||
panic("unexpected SetDNS call")
|
panic("unexpected SetDNS call")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*mockControl) DoNoiseRequest(*http.Request) (*http.Response, error) {
|
||||||
|
panic("unexpected DoNoiseRequest call")
|
||||||
|
}
|
||||||
|
|
||||||
// A very precise test of the sequence of function calls generated by
|
// A very precise test of the sequence of function calls generated by
|
||||||
// ipnlocal.Local into its controlclient instance, and the events it
|
// ipnlocal.Local into its controlclient instance, and the events it
|
||||||
// produces upstream into the UI.
|
// produces upstream into the UI.
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
@ -26,6 +27,7 @@ import (
|
|||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
@ -200,19 +202,40 @@ func (srv *server) handleSSH(s ssh.Session) {
|
|||||||
s.Exit(1)
|
s.Exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if action.Message != "" {
|
|
||||||
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
|
// Loop processing/fetching Actions until one reaches a
|
||||||
}
|
// terminal state (Accept, Reject, or invalid Action), or
|
||||||
if action.Reject {
|
// until fetchSSHAction times out due to the context being
|
||||||
logf("ssh: access denied for %q from %v", ci.uprof.LoginName, ci.src.IP())
|
// done (client disconnect) or its 30 minute timeout passes.
|
||||||
s.Exit(1)
|
// (Which is a long time for somebody to see login
|
||||||
return
|
// instructions and go to a URL to do something.)
|
||||||
}
|
ProcessAction:
|
||||||
if !action.Accept || action.HoldAndDelegate != "" {
|
for {
|
||||||
fmt.Fprintf(s, "TODO: other SSHAction outcomes")
|
if action.Message != "" {
|
||||||
s.Exit(1)
|
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
|
||||||
return
|
}
|
||||||
|
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)
|
lu, err := user.Lookup(localUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logf("ssh: user Lookup %q: %v", localUser, err)
|
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)
|
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) {
|
func (srv *server) handleSessionTermination(ctx context.Context, s ssh.Session, ci *sshConnInfo, cmd *exec.Cmd, exitOnce *sync.Once) {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
// Either the process has already existed, in which case this does nothing.
|
// Either the process has already existed, in which case this does nothing.
|
||||||
|
@ -1615,10 +1615,14 @@ type SSHAction struct {
|
|||||||
// before being forcefully terminated.
|
// before being forcefully terminated.
|
||||||
SesssionDuration time.Duration `json:"sessionDuration,omitempty"`
|
SesssionDuration time.Duration `json:"sessionDuration,omitempty"`
|
||||||
|
|
||||||
// HoldAndDelegate, if non-empty, is a URL that serves an outcome verdict.
|
// HoldAndDelegate, if non-empty, is a URL that serves an
|
||||||
// The connection will be accepted and will block until the
|
// outcome verdict. The connection will be accepted and will
|
||||||
// provided long-polling URL serves a new SSHAction JSON
|
// block until the provided long-polling URL serves a new
|
||||||
// value.
|
// SSHAction JSON value. The URL must be fetched using the
|
||||||
|
// Noise transport (in package control/control{base,http}).
|
||||||
|
// If the long poll breaks before returning a complete HTTP
|
||||||
|
// response, it should be re-fetched as long as the SSH
|
||||||
|
// session is open.
|
||||||
HoldAndDelegate string `json:"holdAndDelegate,omitempty"`
|
HoldAndDelegate string `json:"holdAndDelegate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user