Skip to content

feat: expose SSH_MSG_USERAUTH_BANNER messages via Connection.getBanners()#494

Open
pdecat wants to merge 1 commit into
connectbot:mainfrom
pdecat:feat/get-banners
Open

feat: expose SSH_MSG_USERAUTH_BANNER messages via Connection.getBanners()#494
pdecat wants to merge 1 commit into
connectbot:mainfrom
pdecat:feat/get-banners

Conversation

@pdecat
Copy link
Copy Markdown

@pdecat pdecat commented May 16, 2026

Summary

Today SSH_MSG_USERAUTH_BANNER messages are read by
AuthenticationManager but stored in a package-private field
(String banner) that no caller outside the library can reach. That
makes it impossible to surface banners to users — most painfully when
the server is a Tailscale-managed sshd
that delivers a web-login URL in a banner and then waits for the user
to finish the web login before sending USERAUTH_SUCCESS. The
authenticateWithNone() call blocks the whole time and the user has
no way to see the URL they need to visit.

This change mirrors the upstream trilead-ssh2 patch in
jenkinsci/trilead-ssh2#206:

  • AuthenticationManager: store banners in a List<String> so
    multiple banners aren't clobbered, and expose a public
    getBanners() that returns a defensive snapshot.
  • Connection: add a public getBanners() that delegates to the
    auth manager, returning an empty list when authentication hasn't
    been attempted yet.

Threading

Neither accessor synchronizes on the Connection or
AuthenticationManager monitor. The blocking authenticateWith*
entry points already hold the Connection monitor for the full
duration of the auth call, so a caller that wants to surface banners
live — Tailscale's URL arrives in a banner before USERAUTH_SUCCESS
— must be able to read them from another thread while the auth call
is still in flight. Synchronization is scoped to the banners list
itself: the auth thread holds the list monitor while appending; the
accessor holds it briefly while copying out a snapshot.

Why now

ConnectBot has issue #1150
open for the Tailscale-banner case. In
connectbot#2211
the workaround uses reflection on the package-private am/banner
fields. Once this API ships in a sshlib release, that reflection block
can be replaced with connection.getBanners().

I've tested this exact change end-to-end against a Tailscale-managed
sshd by publishing this branch as 2.2.47-SNAPSHOT to mavenLocal,
pointing a connectbot debug build at it, and swapping the reflection
block for a polling loop over connection.banners: the
https://login.tailscale.com/a/... URL now appears in the terminal
during the auth wait and is also picked up by URL Scan. (See screenshot
on the linked ConnectBot PR.)

Compatibility

  • No source- or binary-breaking changes: the package-private
    String banner field is replaced with a package-private
    List<String> banners. That field was never part of the public API.
  • New public methods only, no removals.

Test plan

  • Added ConnectionTest#testGetBannersReturnsEmptyBeforeAuthentication
    verifying getBanners() returns a non-null, empty list when
    am hasn't been instantiated yet.
  • ./gradlew compileJava passes (pre-existing AccessController
    deprecation warning only, unrelated).
  • ./gradlew test --tests "com.trilead.ssh2.ConnectionTest" passes.
  • ./gradlew spotlessCheck passes.
  • End-to-end Tailscale test from a connectbot debug build pinned
    to a local SNAPSHOT of this branch (see Why now).

…rs()

Servers can send banner messages at any point before authentication
completes — typically a login notice, or for managed-SSH offerings
like Tailscale, a web URL the user must visit to finish authenticating.
Today these banners are received and stored, but the field is package-
private on `AuthenticationManager` and inaccessible from outside the
library, leaving no way for callers to surface them to users.

Mirror the upstream trilead-ssh2 change (trilead/trilead-ssh2#206):
- `AuthenticationManager`: accumulate banners into a `List<String>`
  instead of clobbering the previous value, and expose `getBanners()`
  returning a defensive snapshot.
- `Connection`: add `getBanners()` that delegates to the auth manager,
  or returns an empty list when authentication hasn't been attempted
  yet.

Threading note: neither accessor synchronizes on the `Connection` or
`AuthenticationManager` monitor. The blocking `authenticateWith*` entry
points already hold the `Connection` monitor for the entire duration
of the auth call, so a caller that wants to surface banners live
(Tailscale's web-login URL arrives in a banner BEFORE USERAUTH_SUCCESS)
must be able to read them from another thread while that call is still
in flight. Synchronization is scoped to the `banners` list itself.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 16, 2026

CLA assistant check
All committers have signed the CLA.

@sonarqubecloud
Copy link
Copy Markdown

@pdecat pdecat marked this pull request as ready for review May 16, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants