wallarm/internal/tunnel/tunnel.go
admin 1a2a0fbd7b feat: Go binary — preflight, state, tunnel over TLS:443
Single-binary wallarm deployment manager:
- shared/ — system detection, validation, connectivity (port of wallarm-lib.sh)
- preflight/ — mandatory checks on every start (OS, arch, disk, memory, cloud)
- state/ — ~/.wallarm/state.json persistence (nodes, deployment type)
- tunnel/ — reverse SSH tunnel over TLS:443 via Zoraxy edge proxy
- cmd/wallarm/main.go — auto-detect state, route to wizard or dashboard

wallarm-docker.sh wrapper delegates to existing ct-* scripts for now.
2026-08-01 09:25:22 +00:00

146 lines
3.8 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
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 {
// Load the private key
var signer ssh.Signer
if len(cfg.KeyBytes) > 0 {
var err error
signer, err = ssh.ParsePrivateKey(cfg.KeyBytes)
if err != nil {
return fmt.Errorf("parse embedded key: %w", err)
}
} else {
keyBytes, err := os.ReadFile(cfg.KeyPath)
if err != nil {
return fmt.Errorf("read key %s: %w", cfg.KeyPath, err)
}
signer, err = ssh.ParsePrivateKey(keyBytes)
if err != nil {
return fmt.Errorf("parse key: %w", err)
}
}
sshConfig := &ssh.ClientConfig{
User: cfg.User,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil // Accept all host keys (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)
}