Skip to content

Commit 39b1f03

Browse files
authored
Add support for jump-proxy to avoid the need of VPN to access VPC (#132)
1 parent 5e3526b commit 39b1f03

3 files changed

Lines changed: 122 additions & 3 deletions

File tree

client/cmd/config_set.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ var configSetCmd = &cobra.Command{
4949
cobra.CheckErr(cfg.SetConfig("identity-file", flags.Lookup("identity-file").Value.String()))
5050
updated = true
5151
}
52+
if flags.Changed("jump-proxy") {
53+
cobra.CheckErr(cfg.SetConfig("jump-proxy", flags.Lookup("jump-proxy").Value.String()))
54+
updated = true
55+
}
56+
if flags.Changed("jump-identity-file") {
57+
cobra.CheckErr(cfg.SetConfig("jump-identity-file", flags.Lookup("jump-identity-file").Value.String()))
58+
updated = true
59+
}
5260
if !updated {
5361
cobra.CheckErr("No config item specified")
5462
}
@@ -60,4 +68,6 @@ func init() {
6068
configSetCmd.Flags().String("default-instance", "", "Configure default instance")
6169
configSetCmd.Flags().String("broker", "", "The address of broker")
6270
configSetCmd.Flags().String("identity-file", "", "The identity file to use for authentication")
71+
configSetCmd.Flags().String("jump-proxy", "", "SSH jump server in user@host format")
72+
configSetCmd.Flags().String("jump-identity-file", "", "The identity file to use for jump server authentication")
6373
}

pkg/clientlib/auth_oss.go

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ package clientlib
1616
import (
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+
55136
func (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
}

pkg/clientlib/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ func InitConfigFlags(cmd *cobra.Command) {
4747
cmd.PersistentFlags().StringVar(&profileInput, "profile", "", "The user profile to use.")
4848
cmd.PersistentFlags().BoolVar(&Debug, "debug", false, "Enable debug mode")
4949
cmd.PersistentFlags().String("identity-file", "", "Path to the private key for SSH authentication")
50+
cmd.PersistentFlags().String("jump-proxy", "", "SSH jump server in user@host format")
51+
cmd.PersistentFlags().String("jump-identity-file", "", "Path to the private key for SSH jump server authentication")
5052

5153
// Legacy flags.
5254
cmd.PersistentFlags().StringVar(&brokerAddrFlag, "broker", "novahub.dev:50051", "broker address")

0 commit comments

Comments
 (0)