SagerNet / SagerNet/sing-box

Tailscale SSH authentication URL can be overwritten by a later auth banner

Open
#4,372 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Go
Stars
38.1k
Forks
4.6k
Avg merge
19d 15h
Merged PRs (30d)
1

Description

Operating system

iOS

Installation type

sing-box for Apple

Version

sing-box 1.14 beta / current sing-box for Apple development UI. Exact build number was not recorded.

Description

When connecting through Tools → Tailscale → peer → Connect via SSH, a Tailscale SSH policy using a check/approval step can send an authorization URL in an SSH authentication banner, followed by another banner or final handshake error.

The current sing-box SSH client keeps only the most recent banner:

var lastBanner string

BannerCallback: func(message string) error {
    lastBanner = message
    // send banner to UI
    return nil
}

On handshake failure it returns only lastBanner:

banner := strings.TrimSpace(lastBanner)
if banner != "" {
    return finishWithError(banner)
}

Therefore, if an earlier banner contains the approval URL and a later banner contains a generic denial or fetch error, the URL is lost.

The Apple client has the same single-value behavior: authBanner is replaced on every onAuthBanner callback. It also shows the banner only while the session phase is .connecting; after the phase becomes .finished, only the final error is displayed.

Observed UI result:

Connection failed: tailscale:
tailnet policy does not permit
you to SSH to this node

No authorization link is available in the finished view.

Reproduction
  1. Configure a Tailscale SSH ACL rule that requires a check/approval step.
  2. Add a Tailscale endpoint in sing-box for Apple and authenticate it.
  3. Open Tools → Tailscale, select a peer, and choose Connect via SSH.
  4. Wait for the SSH authentication flow to emit an approval URL and then a later banner/error.
Expected behavior
  • Preserve all SSH authentication banners, or at least preserve the first URL-bearing banner separately.
  • Keep the approval URL visible and clickable after the SSH session enters the finished/error state.
Actual behavior

Only the latest banner/error remains, so an earlier Tailscale SSH approval URL can disappear and the user cannot complete the approval flow from the UI.

Relevant code

Core:

  • daemon/started_service_tailscale_ssh.go
    • stores only lastBanner
    • returns only the last banner on handshake failure

Apple client:

  • ApplicationLibrary/Views/Terminal/TerminalWrapperViewModel.swift
    • stores a single authBanner
    • replaces it for each callback
  • ApplicationLibrary/Views/Terminal/TerminalSessionContentView.swift
    • shows authBanner only during .connecting
    • finished state shows only the final error
Suggested direction

Accumulate unique banners in the core and/or expose the URL separately. In the Apple client, retain the first detected URL and show an explicit Open Tailscale SSH authentication link in both connecting and finished states.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with daemon/started_service_tailscale_ssh.go and trace how authentication banners are stored and returned on handshake failure. Then read TerminalWrapperViewModel.swift and TerminalSessionContentView.swift to follow banner callbacks and the connecting versus finished UI states. Reproduce the approval flow and verify that the authorization URL remains available and clickable after the session fails.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, swift
Domain
backend, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.