fix: retry ssh fallback ports for desktop paths

This commit is contained in:
Peter Steinberger 2026-05-07 14:52:21 +01:00
parent 7884b1d71f
commit fdef9df8af
No known key found for this signature in database
5 changed files with 116 additions and 27 deletions

View File

@ -19,6 +19,7 @@
- Fixed Code and WebVNC bridge websocket auth so upgraded brokers receive short-lived bridge tickets in the `Authorization` header instead of logging them in URL query strings, while preserving query fallback for older brokers.
- Fixed managed AWS macOS desktop leases so readiness and WebVNC use a writable `ec2-user` work root, call `crabbox-ready` by absolute path, and read the generated Screen Sharing password via sudo.
- Fixed coordinator-backed `crabbox list` so a stale admin token no longer blocks normal logged-in users; the CLI now falls back to active user-visible leases instead of failing with `401 unauthorized`.
- Fixed desktop, screenshot, VNC, and WebVNC SSH helpers so they retry live fallback ports when a coordinator lease advertises an SSH port that is not ready yet.
## 0.6.0 - 2026-05-07

View File

@ -129,6 +129,9 @@ func (a App) resolveNetworkLeaseTarget(ctx context.Context, cfg Config, id strin
return Server{}, SSHTarget{}, "", err
}
target = resolved.Target
if target.Host != "" {
_ = probeSSHTransport(ctx, &target, 4*time.Second)
}
if printFallback && resolved.FallbackReason != "" {
fmt.Fprintf(a.Stderr, "network fallback %s\n", resolved.FallbackReason)
}

View File

@ -96,18 +96,32 @@ func captureDesktopScreenshot(ctx context.Context, target SSHTarget, outputPath
func runSSHToWriter(ctx context.Context, target SSHTarget, remote string, stdout io.Writer) error {
remote = wrapRemoteForTarget(target, remote)
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
cmd.Stdout = stdout
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
message := strings.TrimSpace(stderr.String())
if message != "" {
return fmt.Errorf("%w: %s", err, message)
var lastErr error
var lastMessage string
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
probe := target
probe.Port = port
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
cmd.Stdout = stdout
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
lastErr = err
lastMessage = strings.TrimSpace(stderr.String())
if shouldRetrySSHPort(err) {
continue
}
if lastMessage != "" {
return fmt.Errorf("%w: %s", err, lastMessage)
}
return err
}
return err
return nil
}
return nil
if lastMessage != "" {
return fmt.Errorf("%w: %s", lastErr, lastMessage)
}
return lastErr
}
func screenshotRemoteCommand(target SSHTarget) string {

View File

@ -233,27 +233,65 @@ func runSSHQuiet(ctx context.Context, target SSHTarget, remote string) error {
func runSSHQuietWithOptions(ctx context.Context, target SSHTarget, remote, connectTimeout, connectionAttempts string) error {
remote = wrapRemoteForTarget(target, remote)
cmd := exec.CommandContext(ctx, "ssh", sshArgsWithOptions(target, remote, connectTimeout, connectionAttempts)...)
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
return cmd.Run()
var lastErr error
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
probe := target
probe.Port = port
cmd := exec.CommandContext(ctx, "ssh", sshArgsWithOptions(probe, remote, connectTimeout, connectionAttempts)...)
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
err := cmd.Run()
if err == nil {
return nil
}
lastErr = err
if !shouldRetrySSHPort(err) {
return err
}
}
return lastErr
}
func runSSHOutput(ctx context.Context, target SSHTarget, remote string) (string, error) {
remote = wrapRemoteForTarget(target, remote)
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
out, err := cmd.Output()
if err != nil {
return "", err
var lastOut []byte
var lastErr error
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
probe := target
probe.Port = port
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
out, err := cmd.Output()
if err == nil {
return strings.TrimSpace(string(out)), nil
}
lastOut = out
lastErr = err
if !shouldRetrySSHPort(err) {
return "", err
}
}
return strings.TrimSpace(string(out)), nil
return strings.TrimSpace(string(lastOut)), lastErr
}
func runSSHCombinedOutput(ctx context.Context, target SSHTarget, remote string) (string, error) {
remote = wrapRemoteForTarget(target, remote)
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
out, err := cmd.CombinedOutput()
return strings.TrimSpace(string(out)), err
var lastOut []byte
var lastErr error
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
probe := target
probe.Port = port
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
out, err := cmd.CombinedOutput()
if err == nil {
return strings.TrimSpace(string(out)), nil
}
lastOut = out
lastErr = err
if !shouldRetrySSHPort(err) {
return strings.TrimSpace(string(out)), err
}
}
return strings.TrimSpace(string(lastOut)), lastErr
}
func runSSHInputQuiet(ctx context.Context, target SSHTarget, remote, input string) error {
@ -262,11 +300,31 @@ func runSSHInputQuiet(ctx context.Context, target SSHTarget, remote, input strin
func runSSHInput(ctx context.Context, target SSHTarget, remote string, input io.Reader, stdout, stderr io.Writer) error {
remote = wrapRemoteForTarget(target, remote)
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
cmd.Stdin = input
cmd.Stdout = stdout
cmd.Stderr = stderr
return cmd.Run()
if input == nil {
input = strings.NewReader("")
}
data, err := io.ReadAll(input)
if err != nil {
return err
}
var lastErr error
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
probe := target
probe.Port = port
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
cmd.Stdin = bytes.NewReader(data)
cmd.Stdout = stdout
cmd.Stderr = stderr
err := cmd.Run()
if err == nil {
return nil
}
lastErr = err
if !shouldRetrySSHPort(err) {
return err
}
}
return lastErr
}
func runSSHStream(ctx context.Context, target SSHTarget, remote string, stdout, stderr io.Writer) int {
@ -291,6 +349,10 @@ func sshArgs(target SSHTarget, remote string) []string {
return sshArgsWithOptions(target, remote, "10", "3")
}
func shouldRetrySSHPort(err error) bool {
return exitCode(err) == 255
}
func sshArgsWithOptions(target SSHTarget, remote, connectTimeout, connectionAttempts string) []string {
return append(sshBaseArgsWithOptions(target, connectTimeout, connectionAttempts),
target.User+"@"+target.Host,

View File

@ -270,6 +270,15 @@ func TestSSHArgsAuthSecretDisablesControlMaster(t *testing.T) {
}
}
func TestShouldRetrySSHPortOnlyForTransportExit(t *testing.T) {
if !shouldRetrySSHPort(exec.Command("sh", "-c", "exit 255").Run()) {
t.Fatal("ssh transport exit 255 should retry fallback ports")
}
if shouldRetrySSHPort(exec.Command("sh", "-c", "exit 7").Run()) {
t.Fatal("remote command failure should not retry fallback ports")
}
}
func TestSSHCommandLineRedactsSecretAuthUser(t *testing.T) {
target := SSHTarget{
User: "tok_live_secret",