@@ -7,13 +7,15 @@ package tailssh
77
88import (
99 "bytes"
10+ "context"
1011 "crypto/ed25519"
1112 "crypto/rand"
1213 "crypto/sha256"
1314 "encoding/json"
1415 "errors"
1516 "fmt"
1617 "io"
18+ "io/ioutil"
1719 "net"
1820 "net/http"
1921 "net/http/httptest"
@@ -324,9 +326,101 @@ func newSSHRule(action *tailcfg.SSHAction) *tailcfg.SSHRule {
324326 }
325327}
326328
329+ // TestSSHRecordingNonInteractive tests that the SSH server records the SSH session
330+ // when the client is not interactive (i.e. no PTY).
331+ // It starts a local SSH server and a recording server. The recording server
332+ // records the SSH session and returns it to the test.
333+ // The test then verifies that the recording has a valid CastHeader, it does not
334+ // validate the contents of the recording.
335+ func TestSSHRecordingNonInteractive (t * testing.T ) {
336+ if runtime .GOOS != "linux" && runtime .GOOS != "darwin" {
337+ t .Skipf ("skipping on %q; only runs on linux and darwin" , runtime .GOOS )
338+ }
339+ var recording []byte
340+ ctx , cancel := context .WithTimeout (context .Background (), time .Second )
341+ recordingServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
342+ defer cancel ()
343+ var err error
344+ recording , err = ioutil .ReadAll (r .Body )
345+ if err != nil {
346+ t .Error (err )
347+ return
348+ }
349+ w .WriteHeader (http .StatusOK )
350+ }))
351+ defer recordingServer .Close ()
352+
353+ state := & localState {
354+ sshEnabled : true ,
355+ matchingRule : newSSHRule (
356+ & tailcfg.SSHAction {
357+ Accept : true ,
358+ Recorders : []netip.AddrPort {
359+ must .Get (netip .ParseAddrPort (recordingServer .Listener .Addr ().String ())),
360+ },
361+ },
362+ ),
363+ }
364+ s := & server {
365+ logf : t .Logf ,
366+ httpc : recordingServer .Client (),
367+ }
368+ defer s .Shutdown ()
369+
370+ src , dst := must .Get (netip .ParseAddrPort ("100.100.100.101:2231" )), must .Get (netip .ParseAddrPort ("100.100.100.102:22" ))
371+ sc , dc := memnet .NewTCPConn (src , dst , 1024 )
372+ s .lb = state
373+
374+ const sshUser = "alice"
375+ cfg := & gossh.ClientConfig {
376+ User : sshUser ,
377+ HostKeyCallback : gossh .InsecureIgnoreHostKey (),
378+ }
379+
380+ var wg sync.WaitGroup
381+ wg .Add (1 )
382+ go func () {
383+ defer wg .Done ()
384+ c , chans , reqs , err := gossh .NewClientConn (sc , sc .RemoteAddr ().String (), cfg )
385+ if err != nil {
386+ t .Errorf ("client: %v" , err )
387+ return
388+ }
389+ client := gossh .NewClient (c , chans , reqs )
390+ defer client .Close ()
391+ session , err := client .NewSession ()
392+ if err != nil {
393+ t .Errorf ("client: %v" , err )
394+ return
395+ }
396+ defer session .Close ()
397+ t .Logf ("client established session" )
398+ _ , err = session .CombinedOutput ("echo Ran echo!" )
399+ if err != nil {
400+ t .Errorf ("client: %v" , err )
401+ }
402+ }()
403+ if err := s .HandleSSHConn (dc ); err != nil {
404+ t .Errorf ("unexpected error: %v" , err )
405+ }
406+ wg .Wait ()
407+
408+ <- ctx .Done () // wait for recording to finish
409+ var ch CastHeader
410+ if err := json .NewDecoder (bytes .NewReader (recording )).Decode (& ch ); err != nil {
411+ t .Fatal (err )
412+ }
413+ if ch .SSHUser != sshUser {
414+ t .Errorf ("SSHUser = %q; want %q" , ch .SSHUser , sshUser )
415+ }
416+ if ch .Command != "echo Ran echo!" {
417+ t .Errorf ("Command = %q; want %q" , ch .Command , "echo Ran echo!" )
418+ }
419+ }
420+
327421func TestSSHAuthFlow (t * testing.T ) {
328- if runtime .GOOS != "linux" {
329- t .Skip ( "Not running on Linux, skipping" )
422+ if runtime .GOOS != "linux" && runtime . GOOS != "darwin" {
423+ t .Skipf ( "skipping on %q; only runs on linux and darwin" , runtime . GOOS )
330424 }
331425 acceptRule := newSSHRule (& tailcfg.SSHAction {
332426 Accept : true ,
0 commit comments