返回 DeepSeek-Reasonix
dial.go
根目录 / internal / remote / dial.go
1 package remote
2
3 import (
4 "context"
5 "fmt"
6 "net"
7 "time"
8
9 "golang.org/x/crypto/ssh"
10
11 "reasonix/internal/netclient"
12 )
13
14 // dialConfig carries everything a single dial (or one hop of a jump chain)
15 // needs. It is assembled by Client.Start from Options.
16 type dialConfig struct {
17 host ResolvedHost
18 auth *AuthOptions // target auth (holds the target's credentials + cache)
19 resolveHop func(string) (ResolvedHost, *AuthOptions, error)
20 hostKeys *HostKeyPolicy
21 dialer netclient.StreamDialer // first-hop transport; nil => direct
22 dialTimeout time.Duration
23 }
24
25 // hopAuthFor returns the auth to use for a jump host. It never carries the
26 // target's Password/Passphrase closures: a jump host must not be authenticated
27 // with the target's stored credentials.
28 func (cfg dialConfig) hopAuthFor(hop ResolvedHost) *AuthOptions {
29 return &AuthOptions{SecretPrompt: cfg.auth.SecretPrompt, DisableAgent: cfg.auth.DisableAgent}
30 }
31
32 func (cfg dialConfig) resolvedHop(raw string) (ResolvedHost, *AuthOptions, error) {
33 if cfg.resolveHop != nil {
34 return cfg.resolveHop(raw)
35 }
36 userName, hostName, port, err := ParseTarget(raw)
37 if err != nil {
38 return ResolvedHost{}, nil, err
39 }
40 hop := ResolvedHost{Name: raw, HostName: hostName, Port: port, User: userName}
41 applyHostDefaults(&hop)
42 return hop, cfg.hopAuthFor(hop), nil
43 }
44
45 // dialSSH establishes an *ssh.Client to cfg.host, walking any ProxyJump chain
46 // left-to-right. The netclient proxy (cfg.dialer) applies only to the first
47 // hop, matching OpenSSH semantics; subsequent hops are dialed through the
48 // preceding hop's SSH connection. Each hop's host key is verified.
49 //
50 // It returns the target client and the ordered list of intermediary clients
51 // (jump hosts) so the caller can close them when the target connection ends.
52 func dialSSH(ctx context.Context, cfg dialConfig) (*ssh.Client, []*ssh.Client, error) {
53 timeout := cfg.dialTimeout
54 if timeout <= 0 {
55 timeout = 15 * time.Second
56 }
57 base := cfg.dialer
58 if base == nil {
59 base = netclient.DialerFunc((&net.Dialer{Timeout: timeout}).DialContext)
60 }
61
62 var hops []*ssh.Client
63 // dialThrough dials addr using either the base transport (first hop) or the
64 // previous SSH hop's context-aware Dial.
65 dialThrough := func(prev *ssh.Client, addr string) (net.Conn, error) {
66 dctx, cancel := context.WithTimeout(ctx, timeout)
67 defer cancel()
68 if prev == nil {
69 return base.DialContext(dctx, "tcp", addr)
70 }
71 return prev.DialContext(dctx, "tcp", addr)
72 }
73
74 var prev *ssh.Client
75 // Resolve and connect each jump host in order.
76 for i, jump := range cfg.host.ProxyJump {
77 hop, hopAuth, err := cfg.resolvedHop(jump)
78 if err != nil {
79 closeAll(hops)
80 return nil, nil, fmt.Errorf("proxy jump %q: %w", jump, err)
81 }
82 conn, derr := dialThrough(prev, hop.Addr())
83 if derr != nil {
84 closeAll(hops)
85 return nil, nil, fmt.Errorf("proxy jump %d (%s): %w", i+1, hop.Label(), derr)
86 }
87 // Each jump host authenticates with its own credential-free auth, so the
88 // target's password_env is never sent upstream to a jump host.
89 client, cerr := newSSHClient(ctx, conn, hop, hopAuth, cfg.hostKeys, timeout)
90 if cerr != nil {
91 closeAll(hops)
92 return nil, nil, fmt.Errorf("proxy jump %d (%s): %w", i+1, hop.Label(), cerr)
93 }
94 hops = append(hops, client)
95 prev = client
96 }
97
98 conn, err := dialThrough(prev, cfg.host.Addr())
99 if err != nil {
100 closeAll(hops)
101 return nil, nil, fmt.Errorf("dial %s: %w", cfg.host.Label(), err)
102 }
103 target, err := newSSHClient(ctx, conn, cfg.host, cfg.auth, cfg.hostKeys, timeout)
104 if err != nil {
105 closeAll(hops)
106 return nil, nil, err
107 }
108 return target, hops, nil
109 }
110
111 // newSSHClient performs the SSH handshake over an established conn. It bounds
112 // the handshake with a deadline (ssh.ClientConfig.Timeout only covers the TCP
113 // dial, not the version/key exchange, so a host that accepts TCP but never
114 // sends a banner would otherwise hang NewClientConn — and Close — forever).
115 func newSSHClient(ctx context.Context, conn net.Conn, host ResolvedHost, auth *AuthOptions, hostKeys *HostKeyPolicy, timeout time.Duration) (*ssh.Client, error) {
116 methods, authCallback, cleanupAuth, err := buildAuthMethods(ctx, host, auth)
117 if err != nil {
118 conn.Close()
119 return nil, err
120 }
121 defer cleanupAuth()
122 hkCallback, err := hostKeys.Callback(ctx, host.Label())
123 if err != nil {
124 conn.Close()
125 return nil, err
126 }
127 hostKeyAlgorithms, err := hostKeys.HostKeyAlgorithms(host.Addr(), conn.RemoteAddr())
128 if err != nil {
129 conn.Close()
130 return nil, err
131 }
132 clientCfg := &ssh.ClientConfig{
133 User: host.User,
134 Auth: methods,
135 AuthCallback: authCallback,
136 HostKeyCallback: hkCallback,
137 HostKeyAlgorithms: hostKeyAlgorithms,
138 Timeout: timeout,
139 }
140 // Bound the handshake even for ProxyJump channel connections, whose
141 // SetDeadline method returns "deadline not supported". A watcher closes the
142 // connection on timeout/cancellation; the acknowledgement prevents a late
143 // watcher from closing a successfully established client.
144 hsCtx, cancel := context.WithTimeout(ctx, handshakeTimeout(timeout))
145 stopWatch := make(chan struct{})
146 watchDone := make(chan struct{})
147 go func() {
148 defer close(watchDone)
149 select {
150 case <-hsCtx.Done():
151 _ = conn.Close()
152 case <-stopWatch:
153 }
154 }()
155 if deadline, ok := hsCtx.Deadline(); ok {
156 _ = conn.SetDeadline(deadline)
157 }
158 c, chans, reqs, err := ssh.NewClientConn(conn, host.Addr(), clientCfg)
159 close(stopWatch)
160 <-watchDone
161 cancel()
162 if err != nil {
163 conn.Close()
164 return nil, classifyDialError(err)
165 }
166 _ = conn.SetDeadline(time.Time{})
167 return ssh.NewClient(c, chans, reqs), nil
168 }
169
170 func handshakeTimeout(dialTimeout time.Duration) time.Duration {
171 if dialTimeout <= 0 {
172 return 15 * time.Second
173 }
174 return dialTimeout
175 }
176
177 func closeAll(clients []*ssh.Client) {
178 for i := len(clients) - 1; i >= 0; i-- {
179 _ = clients[i].Close()
180 }
181 }
182
182 lines GO