@@ -16,7 +16,9 @@ package clientlib
1616import (
1717 "context"
1818 "fmt"
19+ "net"
1920 "os"
21+ "strings"
2022
2123 "github.com/spf13/cobra"
2224 "golang.org/x/crypto/ssh"
@@ -52,6 +54,85 @@ func (o OssAuthProvider) BindSession(ctx context.Context, session *proto.Session
5254 return ctx
5355}
5456
57+ type jumpConn struct {
58+ net.Conn
59+ client * ssh.Client
60+ }
61+
62+ func (jc * jumpConn ) Close () error {
63+ jc .Conn .Close ()
64+ return jc .client .Close ()
65+ }
66+
67+ // dialJumpServer establishes a connection through an SSH jump server
68+ func dialJumpServer (jumpProxy , jumpIdentityFile , targetHost string , targetPort int ) (net.Conn , error ) {
69+ // Parse jump proxy in format user@host. Use last '@' to support any '@' in user if present.
70+ at := strings .LastIndex (jumpProxy , "@" )
71+ if at <= 0 || at == len (jumpProxy )- 1 {
72+ return nil , fmt .Errorf ("invalid jump-proxy format, expected user@host, got: %s" , jumpProxy )
73+ }
74+ jumpUser := strings .TrimSpace (jumpProxy [:at ])
75+ jumpHost := strings .TrimSpace (jumpProxy [at + 1 :])
76+
77+ // Validate non-empty parts
78+ if jumpUser == "" {
79+ return nil , fmt .Errorf ("invalid jump-proxy: username is empty in %s" , jumpProxy )
80+ }
81+ if jumpHost == "" {
82+ return nil , fmt .Errorf ("invalid jump-proxy: host is empty in %s" , jumpProxy )
83+ }
84+
85+ if _ , _ , err := net .SplitHostPort (jumpHost ); err != nil {
86+ if ! strings .Contains (err .Error (), "missing port in address" ) {
87+ return nil , fmt .Errorf ("invalid jump-proxy host: %v" , err )
88+ }
89+ // Add default SSH port if not specified
90+ jumpHost = fmt .Sprintf ("%s:22" , jumpHost )
91+ }
92+
93+ // Read jump server identity file
94+ var authMethods []ssh.AuthMethod
95+ if jumpIdentityFile != "" {
96+ keyData , err := os .ReadFile (jumpIdentityFile )
97+ if err != nil {
98+ return nil , fmt .Errorf ("error reading jump server SSH key file %s: %v" , jumpIdentityFile , err )
99+ }
100+
101+ key , err := ssh .ParsePrivateKey (keyData )
102+ if err != nil {
103+ return nil , fmt .Errorf ("error parsing jump server SSH key file %s: %v" , jumpIdentityFile , err )
104+ }
105+ authMethods = append (authMethods , ssh .PublicKeys (key ))
106+ } else {
107+ return nil , fmt .Errorf ("jump server identity file is required" )
108+ }
109+
110+ // Configure jump server client
111+ jumpConfig := & ssh.ClientConfig {
112+ User : jumpUser ,
113+ Auth : authMethods ,
114+ HostKeyCallback : ssh .InsecureIgnoreHostKey (), // For jump server
115+ }
116+
117+ // Connect to jump server
118+ DebugLog ("Connecting to jump server: %s@%s" , jumpUser , jumpHost )
119+ jumpClient , err := ssh .Dial ("tcp" , jumpHost , jumpConfig )
120+ if err != nil {
121+ return nil , fmt .Errorf ("error dialing jump server: %v" , err )
122+ }
123+
124+ // Dial target through jump server
125+ targetAddr := fmt .Sprintf ("%s:%d" , targetHost , targetPort )
126+ DebugLog ("Dialing target %s through jump server" , targetAddr )
127+ conn , err := jumpClient .Dial ("tcp" , targetAddr )
128+ if err != nil {
129+ jumpClient .Close ()
130+ return nil , fmt .Errorf ("error dialing target through jump server: %v" , err )
131+ }
132+
133+ return & jumpConn {Conn : conn , client : jumpClient }, nil
134+ }
135+
55136func (o OssAuthProvider ) SshDial (cmd * cobra.Command , sshConn * proto.ExecutionStatus_SshConnection , user string ) (* SshClient , error ) {
56137 hostKey , err := ssh .ParsePublicKey (sshConn .HostKey )
57138 if err != nil {
@@ -88,9 +169,35 @@ func (o OssAuthProvider) SshDial(cmd *cobra.Command, sshConn *proto.ExecutionSta
88169 HostKeyCallback : ssh .FixedHostKey (hostKey ),
89170 }
90171
91- client , err := ssh .Dial ("tcp" , fmt .Sprintf ("%s:%d" , sshConn .Host , sshConn .Port ), clientConfig )
92- if err != nil {
93- return nil , fmt .Errorf ("Error dialing ssh: %v" , err )
172+ // Check if jump server is configured
173+ var jumpProxy , jumpIdentityFile string
174+ if ! IsInSession () {
175+ jumpProxy , _ = GetFlagValue (cmd , "jump-proxy" )
176+ jumpIdentityFile , _ = GetFlagValue (cmd , "jump-identity-file" )
177+ }
178+
179+ var client * ssh.Client
180+ if jumpProxy != "" {
181+ // Use jump server to connect
182+ DebugLog ("Using jump server: %s" , jumpProxy )
183+ conn , err := dialJumpServer (jumpProxy , jumpIdentityFile , sshConn .Host , int (sshConn .Port ))
184+ if err != nil {
185+ return nil , err
186+ }
187+
188+ // Create SSH client using the connection through jump server
189+ sshConn2 , chans , reqs , err := ssh .NewClientConn (conn , fmt .Sprintf ("%s:%d" , sshConn .Host , sshConn .Port ), clientConfig )
190+ if err != nil {
191+ conn .Close ()
192+ return nil , fmt .Errorf ("Error creating SSH client connection through jump server: %v" , err )
193+ }
194+ client = ssh .NewClient (sshConn2 , chans , reqs )
195+ } else {
196+ // Direct connection
197+ client , err = ssh .Dial ("tcp" , fmt .Sprintf ("%s:%d" , sshConn .Host , sshConn .Port ), clientConfig )
198+ if err != nil {
199+ return nil , fmt .Errorf ("Error dialing ssh: %v" , err )
200+ }
94201 }
95202 return & SshClient {Client : client , ShutdownMessage : "" }, nil
96203}
0 commit comments