Tailscale SSH authentication URL can be overwritten by a later auth banner
Nobody has claimed this yet.
- 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
- Configure a Tailscale SSH ACL rule that requires a check/approval step.
- Add a Tailscale endpoint in sing-box for Apple and authenticate it.
- Open Tools → Tailscale, select a peer, and choose Connect via SSH.
- 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
- stores only
Apple client:
ApplicationLibrary/Views/Terminal/TerminalWrapperViewModel.swift- stores a single
authBanner - replaces it for each callback
- stores a single
ApplicationLibrary/Views/Terminal/TerminalSessionContentView.swift- shows
authBanneronly during.connecting - finished state shows only the final error
- shows
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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