forked from gravitational/teleport
/
agent_dialer.go
168 lines (148 loc) · 5.44 KB
/
agent_dialer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package reversetunnel
import (
"context"
"strings"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
apidefaults "github.com/gravitational/teleport/api/defaults"
tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh"
"github.com/gravitational/teleport/api/types"
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/proxy"
)
const proxyAlreadyClaimedError = "proxy already claimed"
// isProxyAlreadyClaimed returns true if the error is non-nil and its message
// ends with "proxy already claimed" (we can't extract a better sentinel out of
// a SSH handshake, unfortunately).
func isProxyAlreadyClaimed(err error) bool {
if err == nil {
return false
}
return strings.HasSuffix(err.Error(), proxyAlreadyClaimedError)
}
// agentDialer dials an ssh server on behalf of an agent.
type agentDialer struct {
client auth.AccessCache
username string
authMethods []ssh.AuthMethod
fips bool
options []proxy.DialerOptionFunc
log logrus.FieldLogger
isClaimed func(principals ...string) bool
}
// DialContext creates an ssh connection to the given address.
func (d *agentDialer) DialContext(ctx context.Context, addr utils.NetAddr) (SSHClient, error) {
// Create a dialer (that respects HTTP proxies) and connect to remote host.
dialer := proxy.DialerFromEnvironment(addr.Addr, d.options...)
pconn, err := dialer.DialTimeout(ctx, addr.AddrNetwork, addr.Addr, apidefaults.DefaultIOTimeout)
if err != nil {
d.log.WithError(err).Debugf("Failed to dial %s.", addr.Addr)
return nil, trace.Wrap(err)
}
var principals []string
callback, err := apisshutils.NewHostKeyCallback(
apisshutils.HostKeyCallbackConfig{
GetHostCheckers: d.hostCheckerFunc(ctx),
OnCheckCert: func(c *ssh.Certificate) error {
if d.isClaimed != nil && d.isClaimed(c.ValidPrincipals...) {
d.log.Debugf("Aborting SSH handshake because the proxy %q is already claimed by some other agent.", c.ValidPrincipals[0])
// the error message must end with
// [proxyAlreadyClaimedError] to be recognized by
// [isProxyAlreadyClaimed]
return trace.Errorf(proxyAlreadyClaimedError)
}
principals = c.ValidPrincipals
return nil
},
FIPS: d.fips,
})
if err != nil {
d.log.Debugf("Failed to create host key callback for %v: %v.", addr.Addr, err)
return nil, trace.Wrap(err)
}
// Build a new client connection. This is done to get access to incoming
// global requests which dialer.Dial would not provide.
conn, chans, reqs, err := tracessh.NewClientConn(ctx, pconn, addr.Addr, &ssh.ClientConfig{
User: d.username,
Auth: d.authMethods,
HostKeyCallback: callback,
Timeout: apidefaults.DefaultIOTimeout,
})
if err != nil {
return nil, trace.Wrap(err)
}
// ssh.NewClient will loop over the global requests channel in a goroutine,
// rejecting all requests; we want to handle the global requests ourselves,
// so we feed it a closed channel to have the goroutine exit immediately.
emptyRequests := make(chan *ssh.Request)
close(emptyRequests)
client := tracessh.NewClient(conn, chans, emptyRequests)
return &sshClient{
Client: client,
requests: reqs,
newChannels: chans,
principals: principals,
}, nil
}
// hostCheckerFunc wraps a apisshutils.CheckersGetter function with a context.
func (d *agentDialer) hostCheckerFunc(ctx context.Context) apisshutils.CheckersGetter {
return func() ([]ssh.PublicKey, error) {
cas, err := d.client.GetCertAuthorities(ctx, types.HostCA, false)
if err != nil {
return nil, trace.Wrap(err)
}
var keys []ssh.PublicKey
for _, ca := range cas {
checkers, err := sshutils.GetCheckers(ca)
if err != nil {
return nil, trace.Wrap(err)
}
keys = append(keys, checkers...)
}
return keys, nil
}
}
// sshClient implements the SSHClient interface.
type sshClient struct {
*tracessh.Client
requests <-chan *ssh.Request
newChannels <-chan ssh.NewChannel
principals []string
}
// NewChannels is a channel that receieves ssh new channel requests.
func (c *sshClient) NewChannels() <-chan ssh.NewChannel {
return c.newChannels
}
// GlobalRequests is a channel that receives global ssh requests.
func (c *sshClient) GlobalRequests() <-chan *ssh.Request {
return c.requests
}
// Principals is a list of principals for the underlying ssh connection.
func (c *sshClient) Principals() []string {
return c.principals
}
// Reply handles replying to a request.
func (c *sshClient) Reply(request *ssh.Request, ok bool, payload []byte) error {
return request.Reply(ok, payload)
}