Startup flow now offers: - [1] Deploy locally — config wizard, deploys on this server - [2] Remote assistance — prompts for jumphost URL, user, password (or SSH key path), opens reverse TLS:443 tunnel so a remote admin can connect and run deployment through their terminal. Tunnel now supports password auth in addition to key auth.
152 lines
4 KiB
Go
152 lines
4 KiB
Go
// Package tunnel provides a reverse SSH tunnel over TLS:443 via a Zoraxy edge proxy.
|
|
// It opens an outbound TLS connection to the configured jumphost, authenticates
|
|
// via SSH, and establishes a reverse port forward so you can reach the target VM
|
|
// through sechpoint.app.
|
|
package tunnel
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// Config holds the tunnel connection parameters.
|
|
type Config struct {
|
|
Jumphost string // e.g., "ssh.sechpoint.app:443"
|
|
RemotePort int // Port on the jumphost that forwards to target's SSH
|
|
LocalSSHPort int // SSH port on the target VM (usually 22)
|
|
User string // SSH user on the jumphost
|
|
Password string // Password auth (takes lowest priority)
|
|
KeyPath string // Path to private key for authentication
|
|
KeyBytes []byte // Raw private key bytes (takes precedence over KeyPath)
|
|
}
|
|
|
|
// DefaultConfig returns a Config with sensible defaults.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
Jumphost: "ssh.sechpoint.app:443",
|
|
RemotePort: 9042,
|
|
LocalSSHPort: 22,
|
|
User: "wallarm-tunnel",
|
|
}
|
|
}
|
|
|
|
// Start opens a reverse SSH tunnel over TLS and keeps it alive.
|
|
// It blocks until SIGINT or connection failure.
|
|
func Start(cfg Config) error {
|
|
// Build auth methods
|
|
var authMethods []ssh.AuthMethod
|
|
|
|
if len(cfg.KeyBytes) > 0 {
|
|
signer, err := ssh.ParsePrivateKey(cfg.KeyBytes)
|
|
if err == nil {
|
|
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
|
}
|
|
}
|
|
if cfg.KeyPath != "" {
|
|
keyBytes, err := os.ReadFile(cfg.KeyPath)
|
|
if err == nil {
|
|
signer, err := ssh.ParsePrivateKey(keyBytes)
|
|
if err == nil {
|
|
authMethods = append(authMethods, ssh.PublicKeys(signer))
|
|
}
|
|
}
|
|
}
|
|
if cfg.Password != "" {
|
|
authMethods = append(authMethods, ssh.Password(cfg.Password))
|
|
}
|
|
|
|
if len(authMethods) == 0 {
|
|
return fmt.Errorf("no authentication method configured (provide key or password)")
|
|
}
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
User: cfg.User,
|
|
Auth: authMethods,
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // trusted infrastructure
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
// TLS dial to the Zoraxy edge (port 443)
|
|
tlsConn, err := tls.Dial("tcp", cfg.Jumphost, &tls.Config{
|
|
InsecureSkipVerify: false,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("TLS dial %s: %w", cfg.Jumphost, err)
|
|
}
|
|
|
|
// SSH over TLS
|
|
sshConn, chans, reqs, err := ssh.NewClientConn(tlsConn, cfg.Jumphost, sshConfig)
|
|
if err != nil {
|
|
tlsConn.Close()
|
|
return fmt.Errorf("SSH handshake: %w", err)
|
|
}
|
|
client := ssh.NewClient(sshConn, chans, reqs)
|
|
defer client.Close()
|
|
|
|
// Request reverse port forward: jumphost:RemotePort -> localhost:LocalSSHPort
|
|
remoteAddr := fmt.Sprintf("0.0.0.0:%d", cfg.RemotePort)
|
|
localAddr := fmt.Sprintf("localhost:%d", cfg.LocalSSHPort)
|
|
|
|
listener, err := client.Listen("tcp", remoteAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("remote listen %s: %w", remoteAddr, err)
|
|
}
|
|
defer listener.Close()
|
|
|
|
fmt.Printf("Tunnel established: %s -> %s\n", remoteAddr, localAddr)
|
|
fmt.Printf("Connect: ssh -p %d root@%s\n", cfg.RemotePort, cfg.Jumphost)
|
|
|
|
// Handle incoming connections on the remote listener
|
|
go func() {
|
|
for {
|
|
remoteConn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go forwardConnection(remoteConn, localAddr)
|
|
}
|
|
}()
|
|
|
|
// Keep alive until signal
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, os.Interrupt)
|
|
|
|
// Heartbeat every 30s
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-sigCh:
|
|
fmt.Println("\nTunnel closed.")
|
|
return nil
|
|
case <-ticker.C:
|
|
_, _, err := client.SendRequest("keepalive@wallarm", true, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("keepalive failed: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func forwardConnection(remoteConn net.Conn, localAddr string) {
|
|
defer remoteConn.Close()
|
|
localConn, err := net.DialTimeout("tcp", localAddr, 10*time.Second)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer localConn.Close()
|
|
|
|
go func() {
|
|
io.Copy(localConn, remoteConn)
|
|
remoteConn.Close()
|
|
}()
|
|
io.Copy(remoteConn, localConn)
|
|
}
|