fix: tunnel credentials now use plain terminal prompts

Bubbletea and huh were fighting over terminal input. Fixed by:
- TUI exits when user selects 'Remote Assistance'
- Plain terminal prompts in main.go collect jumphost URL, user, pass
- Starts tunnel directly, shows connection string for admin
- No more conflicting rendering between TUI and forms
This commit is contained in:
admin 2026-08-01 14:54:08 +00:00
parent c169887fed
commit 8b610b1bdb
2 changed files with 79 additions and 121 deletions

View file

@ -10,23 +10,84 @@
package main package main
import ( import (
"bufio"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"strings"
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight" "git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel" "git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
"git.sechpoint.app/customer-engineering/wallarm/internal/ui" "git.sechpoint.app/customer-engineering/wallarm/internal/ui"
) )
// Embedded tunnel key — set at build time with: // Embedded tunnel key for --tunnel flag (set at build time with -ldflags).
//
// go build -ldflags "-X main.tunnelKey=$(cat ~/.wallarm/tunnel_key)"
var tunnelKey string var tunnelKey string
// Version is set at build time with -ldflags "-X main.version=1.0.0" // Version set at build time.
var version = "dev" var version = "dev"
// runTunnelFlow prompts for tunnel credentials in plain terminal and starts the tunnel.
func runTunnelFlow() {
fmt.Println()
fmt.Println("🔗 Remote Assistance — Secure Tunnel Setup")
fmt.Println()
fmt.Println("The tunnel lets a remote admin connect to this server through your jumphost.")
fmt.Println("Enter the SSH endpoint where the admin will connect:")
fmt.Println()
reader := bufio.NewReader(os.Stdin)
fmt.Print("Jumphost URL [ssh.sechpoint.app:443]: ")
host, _ := reader.ReadString('\n')
host = strings.TrimSpace(host)
if host == "" {
host = "ssh.sechpoint.app:443"
}
fmt.Print("Username [wallarm-tunnel]: ")
user, _ := reader.ReadString('\n')
user = strings.TrimSpace(user)
if user == "" {
user = "wallarm-tunnel"
}
fmt.Print("Password or SSH key path: ")
pass, _ := reader.ReadString('\n')
pass = strings.TrimSpace(pass)
if pass == "" {
fmt.Println("No credentials provided. Aborting.")
return
}
fmt.Println()
fmt.Println("Starting tunnel...")
cfg := tunnel.Config{
Jumphost: host,
User: user,
RemotePort: 9042,
LocalSSHPort: 22,
}
if strings.HasPrefix(pass, "/") {
cfg.KeyPath = pass
} else {
cfg.Password = pass
}
fmt.Println("Share this with your admin:")
fmt.Println()
fmt.Printf(" ssh -p 9042 %s@%s\n", user, host)
fmt.Println()
fmt.Println("Press Ctrl+C to close the tunnel.")
if err := tunnel.Start(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Tunnel error: %v\n", err)
os.Exit(1)
}
}
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, `wallarm Wallarm Deployment Manager fmt.Fprintf(os.Stderr, `wallarm Wallarm Deployment Manager
@ -95,8 +156,12 @@ deployment. On subsequent runs, it shows your existing deployments.
fmt.Println() fmt.Println()
// 2. Launch bubbletea TUI (handles wizard vs dashboard routing) // 2. Launch bubbletea TUI (handles wizard vs dashboard routing)
if err := ui.Run(result); err != nil { err := ui.Run(result)
if err != nil {
fmt.Fprintf(os.Stderr, "UI error: %v\n", err) fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
os.Exit(1) os.Exit(1)
} }
// 3. If TUI exited cleanly (user chose Remote Assistance), start tunnel flow
runTunnelFlow()
} }

View file

@ -15,7 +15,6 @@ import (
"git.sechpoint.app/customer-engineering/wallarm/internal/native" "git.sechpoint.app/customer-engineering/wallarm/internal/native"
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight" "git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
"git.sechpoint.app/customer-engineering/wallarm/internal/state" "git.sechpoint.app/customer-engineering/wallarm/internal/state"
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
) )
// Styles // Styles
@ -35,9 +34,6 @@ type Model struct {
height int height int
status string // feedback message during deployment status string // feedback message during deployment
deploying bool // true while deployment is in progress deploying bool // true while deployment is in progress
tunnelUser string // tunnel credentials (collected in remote config)
tunnelPass string
tunnelHost string
} }
type stateView int type stateView int
@ -45,9 +41,7 @@ type stateView int
const ( const (
viewPreflight stateView = iota viewPreflight stateView = iota
viewRoute viewRoute
viewDeployChoice // new: local deploy or remote assist? viewDeployChoice // local deploy or remote assist?
viewRemoteConfig // new: prompt for tunnel credentials
viewTunnelActive // new: tunnel is running, waiting for remote admin
viewWizard viewWizard
viewDashboard viewDashboard
viewDone viewDone
@ -90,15 +84,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
case "2": case "2":
if m.state == viewDeployChoice { if m.state == viewDeployChoice {
m.state = viewRemoteConfig return m, func() tea.Msg { return tea.Quit() } // exit TUI for tunnel flow
return m, nil
}
case "s":
if m.state == viewRemoteConfig && m.tunnelHost == "" {
return m, m.collectTunnelCreds()
}
if m.state == viewRemoteConfig && m.tunnelHost != "" {
return m, m.startTunnel()
} }
case "enter": case "enter":
if m.state == viewDone || m.state == viewError { if m.state == viewDone || m.state == viewError {
@ -115,14 +101,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state = viewDone m.state = viewDone
} }
return m, nil return m, nil
case tunnelCredsMsg:
if msg.err != nil {
m.status = fmt.Sprintf("Could not collect credentials: %v", msg.err)
m.state = viewError
} else {
m.state = viewRemoteConfig // refresh to show collected creds
}
return m, nil
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
m.height = msg.Height m.height = msg.Height
@ -145,10 +123,6 @@ func (m Model) View() string {
return "" return ""
case viewDeployChoice: case viewDeployChoice:
return deployChoiceView(m) return deployChoiceView(m)
case viewRemoteConfig:
return remoteConfigView(m)
case viewTunnelActive:
return tunnelActiveView(m)
case viewWizard: case viewWizard:
return wizardView(m) return wizardView(m)
case viewDashboard: case viewDashboard:
@ -161,57 +135,6 @@ func (m Model) View() string {
return "" return ""
} }
// collectTunnelCreds runs a huh form to collect tunnel endpoint details.
func (m Model) collectTunnelCreds() tea.Cmd {
return func() tea.Msg {
var host, user, pass string
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Jumphost URL").
Placeholder("ssh.sechpoint.app:443").
Value(&host),
huh.NewInput().
Title("Username").
Placeholder("wallarm-tunnel").
Value(&user),
huh.NewInput().
Title("Password (or SSH key path)").
Placeholder("password or /path/to/key").
EchoMode(huh.EchoModePassword).
Value(&pass),
),
).WithTheme(huh.ThemeCharm()).Run()
m.tunnelHost = host
m.tunnelUser = user
m.tunnelPass = pass
return tunnelCredsMsg{err: err}
}
}
type tunnelCredsMsg struct{ err error }
// startTunnel opens the reverse SSH tunnel with collected credentials.
func (m Model) startTunnel() tea.Cmd {
return func() tea.Msg {
cfg := tunnel.Config{
Jumphost: m.tunnelHost,
User: m.tunnelUser,
Password: m.tunnelPass,
RemotePort: 9042,
LocalSSHPort: 22,
}
// If password looks like a file path, use key auth instead
if strings.HasPrefix(m.tunnelPass, "/") {
cfg.Password = ""
cfg.KeyPath = m.tunnelPass
}
err := tunnel.Start(cfg)
return deployCompleteMsg{err: err}
}
}
func (m Model) startDeploy() tea.Cmd { func (m Model) startDeploy() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
m.deploying = true m.deploying = true
@ -246,36 +169,6 @@ func deployChoiceView(m Model) string {
return s return s
} }
// ─── Remote Config ───────────────────────────────────────────────────
func remoteConfigView(m Model) string {
s := titleStyle.Render("🔗 Remote Assistance Setup") + "\n\n"
s += "Enter the tunnel endpoint where the remote admin will connect:\n\n"
if m.tunnelHost == "" {
s += dimStyle.Render(" (Form will appear below — enter values and press Enter to continue)") + "\n"
s += dimStyle.Render(" Press 's' to start the tunnel after filling in credentials") + "\n"
} else {
s += fmt.Sprintf(" Jumphost: %s\n", goodStyle.Render(m.tunnelHost))
s += fmt.Sprintf(" User: %s\n", m.tunnelUser)
s += dimStyle.Render("\n Tunnel credentials collected. Press 's' to start.") + "\n"
}
return s
}
// ─── Tunnel Active ───────────────────────────────────────────────────
func tunnelActiveView(m Model) string {
s := titleStyle.Render("🔗 Tunnel Active") + "\n\n"
s += goodStyle.Render("Secure tunnel is running.") + "\n\n"
s += "Share this with your admin:\n"
s += dimStyle.Render(" ┌─────────────────────────────────────────┐") + "\n"
s += fmt.Sprintf(" │ %s", activeStyle.Render("ssh -p 9042 root@"+m.tunnelHost)) + dimStyle.Render(" │") + "\n"
s += dimStyle.Render(" └─────────────────────────────────────────┘") + "\n\n"
s += dimStyle.Render("The admin will see the deployment wizard on their terminal.") + "\n"
s += dimStyle.Render("Tunnel stays open until you press q.") + "\n"
return s
}
func wizardView(m Model) string { func wizardView(m Model) string {
s := titleStyle.Render("🛡️ Wallarm Setup Wizard") + "\n" s := titleStyle.Render("🛡️ Wallarm Setup Wizard") + "\n"
@ -289,7 +182,7 @@ func wizardView(m Model) string {
s += activeStyle.Render("Wallarm Native Node Deployment") + "\n" s += activeStyle.Render("Wallarm Native Node Deployment") + "\n"
s += " Press Enter to begin configuration" + "\n\n" s += " Press Enter to begin configuration" + "\n\n"
s += activeStyle.Render("Step 2: Cloud region") + "\n" s += activeStyle.Render("Cloud region:") + "\n"
r := preflightResult r := preflightResult
if r.USReachable { if r.USReachable {
s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n" s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n"
@ -301,7 +194,7 @@ func wizardView(m Model) string {
} else { } else {
s += dimStyle.Render(" EU — not reachable") + "\n" s += dimStyle.Render(" EU — not reachable") + "\n"
} }
s += dimStyle.Render("\nPress q to quit") s += dimStyle.Render("\nPress Enter to continue, q to quit")
return s return s
} }