| 1 | package remote |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "net" |
| 7 | "testing" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | func TestClientUsesResolvedPerHopCredentials(t *testing.T) { |
| 12 | targetAuth := AuthOptions{Password: func() (string, error) { return "target", nil }} |
| 13 | hopAuth := AuthOptions{Password: func() (string, error) { return "jump", nil }} |
| 14 | c, err := New(Options{ |
| 15 | Host: ResolvedHost{HostName: "target", Port: 22, User: "target-user", ProxyJump: []string{"bastion"}}, |
| 16 | Auth: targetAuth, |
| 17 | JumpHosts: []JumpHostOptions{{Host: ResolvedHost{HostName: "10.0.0.8", Port: 2202, User: "jump-user"}, Auth: hopAuth}}, |
| 18 | }) |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | hop, auth, err := c.resolveHop("bastion") |
| 23 | if err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | if hop.Addr() != "10.0.0.8:2202" || hop.User != "jump-user" { |
| 27 | t.Fatalf("resolved hop = %+v", hop) |
| 28 | } |
| 29 | if auth.Password == nil { |
| 30 | t.Fatal("configured jump password was dropped") |
| 31 | } |
| 32 | jumpSecret, err := auth.Password() |
| 33 | if err != nil || jumpSecret != "jump" { |
| 34 | t.Fatalf("jump password = %q, %v", jumpSecret, err) |
| 35 | } |
| 36 | targetSecret, _ := targetAuth.Password() |
| 37 | if jumpSecret == targetSecret { |
| 38 | t.Fatal("jump auth reused the target credential") |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | func TestClientKeepsAliasCredentialsDistinctForSharedEndpoint(t *testing.T) { |
| 43 | password := func(value string) func() (string, error) { |
| 44 | return func() (string, error) { return value, nil } |
| 45 | } |
| 46 | endpoint := ResolvedHost{HostName: "10.0.0.8", Port: 22, User: "jump-user"} |
| 47 | c, err := New(Options{ |
| 48 | Host: ResolvedHost{HostName: "target", Port: 22, User: "target-user", ProxyJump: []string{"primary", "backup"}}, |
| 49 | JumpHosts: []JumpHostOptions{ |
| 50 | {Host: endpoint, Auth: AuthOptions{Password: password("primary-secret")}}, |
| 51 | {Host: endpoint, Auth: AuthOptions{Password: password("backup-secret")}}, |
| 52 | }, |
| 53 | }) |
| 54 | if err != nil { |
| 55 | t.Fatal(err) |
| 56 | } |
| 57 | _, primary, err := c.resolveHop("primary") |
| 58 | if err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | _, backup, err := c.resolveHop("backup") |
| 62 | if err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | primarySecret, _ := primary.Password() |
| 66 | backupSecret, _ := backup.Password() |
| 67 | if primarySecret != "primary-secret" || backupSecret != "backup-secret" { |
| 68 | t.Fatalf("shared endpoint credentials collided: primary=%q backup=%q", primarySecret, backupSecret) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | type noDeadlineConn struct{ net.Conn } |
| 73 | |
| 74 | func (noDeadlineConn) SetDeadline(time.Time) error { return errors.New("unsupported") } |
| 75 | func (noDeadlineConn) SetReadDeadline(time.Time) error { return errors.New("unsupported") } |
| 76 | func (noDeadlineConn) SetWriteDeadline(time.Time) error { return errors.New("unsupported") } |
| 77 | |
| 78 | func TestSSHHandshakeHonorsContextWhenDeadlinesUnsupported(t *testing.T) { |
| 79 | client, server := net.Pipe() |
| 80 | defer client.Close() |
| 81 | defer server.Close() |
| 82 | ctx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond) |
| 83 | defer cancel() |
| 84 | done := make(chan error, 1) |
| 85 | go func() { |
| 86 | _, err := newSSHClient(ctx, noDeadlineConn{client}, |
| 87 | ResolvedHost{HostName: "target", Port: 22, User: "u"}, |
| 88 | &AuthOptions{DisableAgent: true}, &HostKeyPolicy{}, time.Second) |
| 89 | done <- err |
| 90 | }() |
| 91 | select { |
| 92 | case err := <-done: |
| 93 | if err == nil { |
| 94 | t.Fatal("banner-less handshake unexpectedly succeeded") |
| 95 | } |
| 96 | case <-time.After(500 * time.Millisecond): |
| 97 | t.Fatal("handshake outlived its context on a ProxyJump-style connection") |
| 98 | } |
| 99 | } |
| 100 |