diff --git a/src/app/[[...mdxPath]]/page.tsx b/src/app/[[...mdxPath]]/page.tsx index 8d2ae3c2..0ed4a6ee 100644 --- a/src/app/[[...mdxPath]]/page.tsx +++ b/src/app/[[...mdxPath]]/page.tsx @@ -1,5 +1,7 @@ import type { FC } from 'react'; import { generateStaticParamsFor, importPage } from 'nextra/pages'; +import { removeLinks } from 'nextra/remove-links'; +import { ArticleNavigation } from '@/components/ArticleNavigation'; import { useMDXComponents as getMDXComponents } from '../../mdx-components'; export const generateStaticParams = generateStaticParamsFor('mdxPath'); @@ -24,6 +26,9 @@ const Page: FC = async (props) => { const { default: MDXContent, toc, metadata, sourceCode } = result; return ( + {(params.mdxPath?.length ?? 0) > 0 && toc.length > 0 && ( + ({ id, depth, title: removeLinks(value) }))} /> + )} ); diff --git a/src/app/styles.css b/src/app/styles.css index e4f1a52f..2e5d98b4 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -857,3 +857,199 @@ html:has(.hero-brand):not([data-brand-docked='true']) a:has(> .navbar-brand) { width: 20px; height: 20px; } + +.article-navigation { + display: none; +} + +@media (width < 1280px) { + html:has(.article-navigation), + html:has(.article-navigation):not(:has(*:focus)) { + scroll-padding-top: calc(var(--site-header-height) + 4.5rem); + } + + html:has(.article-navigation) .nextra-sidebar { + display: none; + } + + article:has(.article-navigation) { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + column-gap: 1rem; + align-content: start; + align-items: center; + } + + article:has(.article-navigation) > * { + grid-column: 1 / -1; + min-width: 0; + } + + article:has(.article-navigation) > .nextra-breadcrumb { + grid-column: 1; + grid-row: 1; + margin: 0; + } + + article:has(.article-navigation) > .nextra-breadcrumb > span:last-child { + overflow: hidden; + text-overflow: ellipsis; + } + + article:has(.article-navigation) > .nextra-border:has(> button) { + grid-column: 2; + grid-row: 1; + float: none; + margin: 0; + } + + article:has(.article-navigation) > #nextra-skip-nav { + position: absolute; + } + + article:has(.article-navigation) > main { + margin-top: 1rem; + } + + .article-navigation { + display: block; + position: sticky; + top: calc(var(--site-header-height) + 0.5rem); + z-index: 20; + margin-bottom: 1.5rem; + font-family: var(--font-source-sans), Arial, sans-serif; + } + + .article-navigation details { + position: relative; + } + + .article-navigation summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + min-height: 44px; + padding: 0.625rem 1rem; + border: 1px solid #8885; + border-radius: 0.5rem; + background: rgb(var(--nextra-bg)); + font-weight: 600; + cursor: pointer; + list-style: none; + } + + .article-navigation summary::-webkit-details-marker { + display: none; + } + + .article-navigation details[open] summary svg { + transform: rotate(180deg); + } + + .article-navigation-panel { + display: grid; + grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); + gap: 1.5rem; + position: absolute; + top: calc(100% + 0.5rem); + left: 0; + right: 0; + max-height: calc(100dvh - var(--site-header-height) - 5.5rem); + overflow-y: auto; + overscroll-behavior: contain; + padding: 1rem; + border: 1px solid #8885; + border-radius: 0.75rem; + background: rgb(var(--nextra-bg)); + box-shadow: 0 8px 24px #0003; + } + + .article-navigation h2 { + margin: 0 0 0.75rem; + font-size: 1rem; + font-weight: 700; + } + + .article-navigation h3 { + margin: 1rem 0 0.375rem; + font-size: 0.8125rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.7; + } + + .article-navigation ol, + .article-navigation ul { + margin: 0; + padding: 0; + list-style: none; + } + + .article-navigation-panel a { + display: flex; + align-items: center; + min-height: 44px; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + color: inherit; + font-size: 0.9375rem; + line-height: 1.4; + text-decoration: none; + } + + .article-navigation-panel a:hover, + .article-navigation-panel a[aria-current] { + background: #8882; + } + + .article-navigation-panel a[aria-current] { + color: #1d4ed8; + font-weight: 600; + } + + .dark .article-navigation-panel a[aria-current] { + color: #93c5fd; + } + + .article-subsection { + padding-left: 0.75rem; + } + + .article-guide-navigation { + border-left: 1px solid #8884; + padding-left: 1rem; + } + + .article-navigation-mobile-label { + display: none; + } + + .article-navigation summary:focus-visible, + .article-navigation a:focus-visible { + outline: 3px solid #438be5; + outline-offset: 3px; + } +} + +@media (width < 768px) { + .article-navigation-panel { + grid-template-columns: minmax(0, 1fr); + } + + .article-guide-navigation, + .article-navigation-tablet-label { + display: none; + } + + .article-navigation-mobile-label { + display: inline; + } +} + +@media print { + .article-navigation { + display: none; + } +} diff --git a/src/components/ArticleNavigation.tsx b/src/components/ArticleNavigation.tsx new file mode 100644 index 00000000..469a7308 --- /dev/null +++ b/src/components/ArticleNavigation.tsx @@ -0,0 +1,132 @@ +'use client'; + +import type { ReactNode } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { useEffect, useRef, useState } from 'react'; +import { guideGroups } from '@/navigation/guides'; + +type ArticleHeading = Readonly<{ + id: string; + depth: number; + title: ReactNode; +}>; + +type ArticleNavigationProps = Readonly<{ + headings: readonly ArticleHeading[]; +}>; + +/** Keep article sections within reach when the desktop sidebars do not fit. */ +export function ArticleNavigation({ headings }: ArticleNavigationProps) { + const pathname = usePathname(); + const detailsRef = useRef(null); + const [activeId, setActiveId] = useState(headings[0]?.id); + + useEffect(() => { + let frame = 0; + const update = () => { + frame = 0; + const navigationBottom = detailsRef.current?.getBoundingClientRect().bottom ?? 0; + let current = headings[0]?.id; + for (const heading of headings) { + const element = document.getElementById(heading.id); + if (element && element.getBoundingClientRect().top <= navigationBottom + 24) { + current = heading.id; + } + } + setActiveId(current); + }; + const schedule = () => { + if (!frame) { + frame = window.requestAnimationFrame(update); + } + }; + schedule(); + window.addEventListener('scroll', schedule, { passive: true }); + window.addEventListener('resize', schedule); + return () => { + window.cancelAnimationFrame(frame); + window.removeEventListener('scroll', schedule); + window.removeEventListener('resize', schedule); + }; + }, [headings]); + + useEffect(() => { + const details = detailsRef.current; + if (!details) { + return; + } + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + details.open = false; + details.querySelector('summary')?.focus(); + } + }; + const onFocusOut = (event: FocusEvent) => { + if (!(event.relatedTarget instanceof Node) || !details.contains(event.relatedTarget)) { + details.open = false; + } + }; + details.addEventListener('keydown', onKeyDown); + details.addEventListener('focusout', onFocusOut); + return () => { + details.removeEventListener('keydown', onKeyDown); + details.removeEventListener('focusout', onFocusOut); + }; + }, []); + + const close = () => { + if (detailsRef.current) { + detailsRef.current.open = false; + } + }; + + return ( +
+
+ + Guides & contents + In this article + + +
+ + +
+
+
+ ); +} diff --git a/src/content/guides/add-host.mdx b/src/content/guides/add-host.mdx index 9fc31bd4..102a90ab 100644 --- a/src/content/guides/add-host.mdx +++ b/src/content/guides/add-host.mdx @@ -16,6 +16,6 @@ You'll need a reachable server with SSH enabled, its hostname or IP address, you After saving, tap the host in the Host List to connect. On your first connection, compare the displayed host-key fingerprint with the fingerprint supplied by your server administrator before accepting it. Then enter your password, or use the SSH key authorized for your account on the server. -If you use a key, import your private key into ConnectBot's key manager and select it for the host. The corresponding public key must already be authorized on the server. +If you use a key, import your private key into ConnectBot's key manager and select it for the host. The corresponding public key must already be authorized on the server. See [SSH key algorithms and biometric protection](/guides/ssh-keys/) for help choosing and protecting a key. Once connected, try `whoami` to check which account you're using. [Keep special keys visible](/guides/special-keys/) for easier terminal input. If the connection fails, [check these common causes](/bugs/#connection-help). diff --git a/src/content/guides/index.mdx b/src/content/guides/index.mdx index 9e480877..9855899e 100644 --- a/src/content/guides/index.mdx +++ b/src/content/guides/index.mdx @@ -16,6 +16,12 @@ Start with a working connection, then make terminal input comfortable. Existing - [Set up Mosh for changing networks](./mosh/). - [Troubleshoot a connection](/bugs/#connection-help). +## SSH keys + +### [Which key algorithm should I use, and what does biometric protection do?](./ssh-keys/) + +Compare RSA, EC/ECDSA, Ed25519, and DSA. Learn how fingerprint-protected Android Keystore keys differ from passphrases and confirmation prompts. + ## Keyboard ### [How do I keep the special keys visible?](./special-keys/) @@ -28,6 +34,14 @@ Keep Ctrl, Esc, arrows, and other terminal keys on screen, even when you are not Save your connection details so your server is ready to open from the Host List. +### [How do local, remote, and dynamic port forwards differ?](./port-forwarding/) + +Choose a tunnel type, check OpenSSH or Dropbear permissions, and troubleshoot listening ports, destination addresses, and SOCKS settings. + +### [How do I automate my post-login routine?](./post-login-automation/) + +Combine text matching, commands, special keys, waits, and port-forward controls into a repeatable startup sequence. Learn how to handle failures and take over manually. + ## Appearance ### [How do I change terminal colors and font size?](./terminal-appearance/) diff --git a/src/content/guides/mosh.mdx b/src/content/guides/mosh.mdx index e95af57b..56bb6636 100644 --- a/src/content/guides/mosh.mdx +++ b/src/content/guides/mosh.mdx @@ -31,6 +31,6 @@ Allow inbound UDP traffic for the Mosh ports on the server's firewall and any in If SSH login works but Mosh stalls, check that `mosh-server` is installed and available on the server's command path, and that UDP traffic can reach it. A network that permits SSH may still block UDP. -Mosh supports interactive terminals; use SSH connections for port forwarding. Mosh can recover while the server process and client session remain alive. For work that should survive closing the app or a server-side session ending, use a terminal multiplexer such as `tmux` on the server. +Mosh supports interactive terminals; use [SSH connections for port forwarding](/guides/port-forwarding/). Mosh can recover while the server process and client session remain alive. For work that should survive closing the app or a server-side session ending, use a terminal multiplexer such as `tmux` on the server. Read more about roaming and network requirements in the [official Mosh documentation](https://mosh.org/), or see [connection help](/bugs/#connection-help). diff --git a/src/content/guides/port-forwarding.mdx b/src/content/guides/port-forwarding.mdx new file mode 100644 index 00000000..860b7aed --- /dev/null +++ b/src/content/guides/port-forwarding.mdx @@ -0,0 +1,178 @@ +--- +title: Local, remote, and dynamic port forwarding +--- + +# Local, remote, and dynamic port forwarding + +Written for **ConnectBot v1.11.1**. + +An SSH port forward carries TCP connections through your SSH session. It can let you reach a server-side service from your phone, expose a phone-side service through your server, or provide a SOCKS proxy for an application. + +**Local and remote describe where the listening port is created—not where you enter the settings.** The destination is the service that receives the connection after it crosses the tunnel. + +## Choose a forwarding type + +| Type | Where you connect | Where the destination service runs | Use it for | +| --- | --- | --- | --- | +| **Local** | A listening port on your phone | On the machine running SSH, or another host reachable from it | Accessing a server-side service from your phone. | +| **Remote** | A listening port on the machine running SSH | On your phone, or another host reachable from your phone | Accessing a phone-side service through your server. | +| **Dynamic (SOCKS)** | A SOCKS proxy port on your phone | Any allowed host reachable from the machine running SSH; your application chooses each destination | Accessing multiple services through one proxy. | + +For these forwards, traffic is encrypted **between the phone and SSH server**. Connections outside that segment still need their own protection, such as HTTPS. These are TCP forwards, not a whole-device VPN or a UDP tunnel. + +## Set up a forward in ConnectBot + +1. [Save an SSH host](/guides/add-host/) and confirm you can sign in. +2. Open that host's menu in the Host List and choose **Edit port forwards**. +3. Add a forward with a nickname, type, and **Source port**. For Local or Remote, also enter a **Destination** as `host:port`. +4. For Remote, choose the server-side **Host address** to listen on. Start with **localhost** for access only from the server itself. +5. Save and connect to the host. If already connected, check the forward's enabled state in the port-forward list. + +Saved forwards are configured to start enabled by default in the current implementation, but creating or enabling a forward still requires an authenticated SSH connection. Keep that connection alive while using the tunnel. Use an **SSH** host for forwarding: ConnectBot's Mosh transport closes its initial SSH connection after handoff. + +The current editor accepts source ports **1–65535**. It does not expose OpenSSH's server-assigned port `0` feature, and Dynamic has no fixed destination field. + +## Local: reach a service from your phone + +Suppose a web service listens on port `80` on the SSH server, but you do not want to open that service directly to the network. + +| ConnectBot field | Example | +| --- | --- | +| Type | Local | +| Source port | `8080` | +| Destination | `127.0.0.1:80` | + +The route is: **phone application → phone port 8080 → SSH tunnel → server port 80**. + +Try `http://127.0.0.1:8080/` in a browser on the phone. The destination's `127.0.0.1` means **the machine running the SSH server**, not the phone. You can also use a destination on the server's network, provided the server can reach it and its forwarding policy allows it. + +ConnectBot currently binds Local and Dynamic listeners to the address returned by Android's `InetAddress.getLocalHost()`. It does not offer a custom listen address for these types. Loopback access is the usual starting point; if it fails, check the actual bound address. Do not assume the listener is available through the phone's Wi-Fi address. + +## Remote: reach a phone-side service from the server + +Suppose an application on your phone listens on port `8000` and you want to reach it from the SSH server. + +| ConnectBot field | Example | +| --- | --- | +| Type | Remote | +| Host address | `localhost` | +| Source port | `18080` | +| Destination | `127.0.0.1:8000` | + +The route is: **server application → server port 18080 → SSH tunnel → phone port 8000**. + +On the server, connect to `http://127.0.0.1:18080/`. For this forward, the destination's `127.0.0.1` means **the phone**. If there is no service listening on the phone at port `8000`, the tunnel has nothing to connect to. + +To allow other machines to use this server-side port, the requested Host address, server policy, and firewall must all permit it. An “all addresses” choice in ConnectBot is a request to the server, not an override of its restrictions. A listener on all interfaces can expose your phone-side service to other people who can reach the server. + +## Dynamic: use a SOCKS proxy + +Choose **Dynamic (SOCKS)** and a Source port such as `1080`. Configure a SOCKS-capable application on the phone to use `127.0.0.1:1080` as its proxy. + +The application chooses the destination for each connection; you do not save a single `host:port` destination in ConnectBot. Each requested connection goes through SSH and is opened from the server. + +Use a **SOCKS** proxy setting, not an HTTP proxy setting. Simply starting the forward does not reroute every app on the phone. If the application supports resolving hostnames through SOCKS, enable that option when you want name resolution to happen through the server rather than on the phone. + +## Does the server allow forwarding? + +A successful shell login does not prove forwarding is allowed. Check the running SSH service, its effective policy, the account/key restrictions, and then test actual traffic. If you do not administer the server, ask its administrator to check these settings. + +### OpenSSH + +On the server, inspect the effective configuration for your connection. Replace these example values with your username, the phone's source address and resolved hostname as seen by the server, and the server's listening address/port: + +```sh +sudo /usr/sbin/sshd -T -C user=alex,addr=198.51.100.23,host=phone.example,laddr=192.0.2.10,lport=22 +``` + +`-T` reports the parsed configuration; `-C` applies matching `Match` rules. Use the daemon's actual config file with `-f` if it runs with a nonstandard one, and account for any startup `-o` overrides. This checks the configuration on disk; it does not prove a running daemon has reloaded it. See the [OpenSSH server manual](https://man.openbsd.org/sshd). + +Look for these entries: + +| Setting | What to check | +| --- | --- | +| `AllowTcpForwarding` | `yes`/`all` permits both directions; `local` permits Local and Dynamic; `remote` permits Remote; `no` blocks both. | +| `DisableForwarding` | `yes` overrides forwarding permissions. | +| `PermitOpen` | Limits server-side destinations for Local and Dynamic. | +| `PermitListen` | Limits Remote listening addresses and ports. | +| `GatewayPorts` | Controls Remote exposure: `no` forces loopback; `clientspecified` allows requested addresses; `yes` forces wildcard binding. | + +`PermitOpen` compares requested host names/addresses, not merely where DNS resolves them. A rule permitting `localhost:80` may not permit a request for `127.0.0.1:80`. Remote listen rules also distinguish `localhost` from explicit loopback addresses. Details are in [sshd_config](https://man.openbsd.org/sshd_config). + +Check the `authorized_keys` entry actually used to log in: `no-port-forwarding`, `restrict`, `permitopen=`, and `permitlisten=` can further limit the connection. SSH certificate restrictions can apply too. Check the server logs while making a request through the forward. + +### Dropbear + +Dropbear generally uses startup arguments rather than OpenSSH's `sshd_config`. On the server, inspect its help and process arguments: + +```sh +dropbear -h +ps -ef +``` + +On a BusyBox-based system, use its supported `ps` options, such as `ps w`. Also inspect the service's startup configuration; routers may generate daemon arguments from their own settings. + +- **`-j`** disables Local forwarding, including Dynamic's server-side connections. +- **`-k`** disables Remote forwarding. +- **`-a`** allows other hosts to connect to remote forwarded ports. + +A key's `no-port-forwarding`, `restrict`, or `permitopen=` options may impose additional limits. Newer versions also document `permitlisten=` for Remote ports; check the manual for the installed version. See the [upstream Dropbear manual](https://github.com/mkj/dropbear/blob/master/manpages/dropbear.8). + +Embedded builds can omit forwarding at compile time. Dropbear's [build options](https://github.com/mkj/dropbear/blob/master/src/default_options.h) include `DROPBEAR_SVR_LOCALTCPFWD` and `DROPBEAR_SVR_REMOTETCPFWD`; a missing feature cannot be enabled with a client setting. + +### Other SSH servers + +Look for TCP forwarding or tunneling permissions for the actual user, key, or role. Do not copy OpenSSH configuration names into a different server. The definitive check is whether the requested listener and destination work for that connection. + +## Test the tunnel, not just the listener + +For the Local example, first test the service **on the server**: + +```sh +curl http://127.0.0.1:80/ +``` + +Then open `http://127.0.0.1:8080/` **on the phone**. For the Remote example, run this **on the server**, while the phone service and SSH session are active: + +```sh +curl http://127.0.0.1:18080/ +``` + +If available, `ss -ltn` on the server shows whether the Remote listener exists and which address it uses. A listener only on `127.0.0.1` or `::1` will not accept connections to the server's public address. + +For a desktop comparison, an OpenSSH client can test the same server/account policy with: + +```sh +ssh -vv -N -o ExitOnForwardFailure=yes -L 127.0.0.1:18080:127.0.0.1:80 alex@server.example +``` + +In a second terminal **on that desktop**, request `http://127.0.0.1:18080/`. These commands create a desktop listener, not a phone listener. `ExitOnForwardFailure` catches setup failures; it does not prove later connections to the destination will succeed. Local and Dynamic listeners can start even if the server later rejects forwarded traffic. + +For Dynamic, test with an application explicitly using SOCKS. With a command-line client available **on the phone**, this requests remote hostname resolution: + +```sh +curl --socks5-hostname 127.0.0.1:1080 https://example.com/ +``` + +ConnectBot does not supply `curl` itself. Running that command inside your SSH terminal runs it on the server, where the phone's SOCKS listener is not located. + +## Common pitfalls + +- **Listening below port 1024.** Ports such as `80` and `443` often require elevated privileges on Unix-like systems. For Local/Dynamic, the listener runs under the Android app's permissions; server-side `sudo` cannot grant it privileges. For Remote, the server/account rules apply. Start with `8080`, `1080`, or `18080`. A high listening port can forward to destination port `80` or `443` without owning that destination listener. Linux's [privileged-port threshold is configurable](https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html), so this is not an unconditional rule on every device or server. +- **Address already in use.** Choose a different Source port or stop the conflicting listener, including another saved forward using that port. +- **IPv6 destination syntax.** The current editor splits Destination on `:` and expects exactly `host:port`. Use a DNS hostname or IPv4 address there; raw IPv6 literals are not accepted, even though Remote Host address has IPv6 choices. +- **Wrong side's localhost.** Local/Dynamic destinations are reached from the server; Remote destinations are reached from the phone. Commands typed into an SSH shell run on the server. +- **Forward marked enabled, but traffic fails.** Check the destination service, forwarding policy, and server logs. A local listener being enabled does not mean its destination has been tested. +- **Remote forward works only on the server.** Check the actual listen address, `GatewayPorts` or Dropbear's `-a`, and inbound firewall rules. NAT or cloud security groups may also block other hosts. +- **Unexpected exposure.** Prefer loopback Remote listeners unless you intend to share the service. SSH authenticates the tunnel owner; a service exposed through a remote listening port still needs appropriate access controls. +- **HTTP/SOCKS or HTTP/HTTPS mismatch.** Use the application's correct proxy type and the service's correct protocol. HTTPS certificates and redirects may expect the original service hostname, not `localhost`. +- **Network changes or an ended session.** These forwards depend on SSH. Reconnect and check they are enabled again after a disconnect; a Mosh session does not preserve the SSH tunnel. + +## Implementation references + +ConnectBot-specific behavior described here is for **v1.11.1**. These source references explain the implementation: + +- [SSH.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/transport/SSH.kt): Local, Remote, and Dynamic setup, listen addresses, and authentication checks. +- [PortForwardEditorViewModel.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/ui/screens/portforwardlist/PortForwardEditorViewModel.kt): source-port range, Remote address selection, and destination fields. +- [PortForward.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/data/entity/PortForward.kt): saved forwarding types and startup state. +- [Mosh.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/transport/Mosh.kt): closing the bootstrap SSH connection. diff --git a/src/content/guides/post-login-automation.mdx b/src/content/guides/post-login-automation.mdx new file mode 100644 index 00000000..e9d8262e --- /dev/null +++ b/src/content/guides/post-login-automation.mdx @@ -0,0 +1,120 @@ +--- +title: Post-login automation +--- + +# Post-login automation + +Written for **ConnectBot v1.11.1**. + +Post-login automation turns a saved host into a repeatable workspace. ConnectBot can wait for a prompt, type a command, press keys, pause, control a port forward, and disconnect—all in the order you choose. + +Use it to open a project directory, attach to a terminal multiplexer, navigate a text-based menu, or wait for a service to announce that it is ready before enabling a tunnel. + +## Set up an action sequence + +1. [Save a host](/guides/add-host/) and check that you can connect normally. +2. Edit the saved host and select **Post-login automation**. A new host must be saved before this option is available. +3. Select **Add action**, choose an action type, and fill in its settings. +4. Save the action. Add more actions, then drag them into order or use **Move up** and **Move down**. You can also edit or delete individual actions. +5. Save the overall sequence and connect to the host to try it. + +Actions belong to that host and run sequentially when its terminal session starts. They run after the connection is established, so they do not replace SSH authentication or answer its initial password, key, or host-verification prompts. A new connection can run the sequence again; make commands safe to repeat. Editing the saved sequence does not change a run already in progress. + +## The available actions + +| Action | What it does | Useful for | +| --- | --- | --- | +| **Wait for string** | Waits for incoming text to contain a literal string or match a regular expression, up to a timeout. | Recognizing a shell prompt, menu, or readiness message. | +| **Send string** | Sends the text you enter to the terminal. | Typing a command, a menu answer, or several lines of input. | +| **Send special key** | Sends a selected key, optionally with Shift, Ctrl, or Alt. | Submitting a command with Enter, moving through a menu, or sending Ctrl+C. | +| **Wait** | Pauses for a fixed duration in seconds. | Giving an application a short settling period when it has no useful readiness message. | +| **Disconnect** | Closes the connection and ends the sequence. | Finishing a short task after confirming its completion. | +| **Enable port forward** | Enables a selected saved forward for this SSH host. | Making a tunnel available at a chosen point in the sequence. | +| **Disable port forward** | Disables a selected saved forward for this SSH host. | Turning off a tunnel when a task no longer needs it. | + +Wait durations and text-wait timeouts must be greater than zero. New **Wait** actions start at one second; new **Wait for string** actions start with a 30-second timeout. Adjust these for your server and connection. + +### Sending text and keys + +**Send string does not add Enter automatically.** For a command, send its text and follow it with **Send special key → Enter**. Actual line breaks in a multiline string are sent as input too; typing the two characters `\n` does not turn them into a newline. + +Special keys include Enter, Tab, Escape, Backspace, Delete, the arrow keys, Home, End, Insert, Page Up, Page Down, and F1–F12. The picker also includes A–Z and selected punctuation so you can make combinations such as **Ctrl+C**, **Ctrl+D**, or **Ctrl+L**. Modifier combinations are sent to the terminal; how a remote application responds depends on that application and its terminal settings. + +Sending input successfully means it was sent, not that a shell command succeeded. Add a text wait when later actions depend on a result. + +### Waiting for text + +Start with a distinctive literal string that you can see in the server's output, such as `Workspace ready`. Literal matching is case-sensitive and can match part of a longer message. With **Regular expression** enabled, the field becomes a pattern using RE2 syntax. Lookaround and backreferences are not supported. + +For example, `(?m)^Workspace ready$` matches that entire line, while `(?m)^READY:[0-9]+$` can match a readiness marker containing a changing number. Leave the regular-expression option off when you want characters such as `$`, `[`, or `.` treated literally. + +ConnectBot matches incoming decoded text with ANSI formatting removed and line endings normalized. It watches the output stream, rather than searching the visible screen or old scrollback. Colorful prompts can work, but full-screen applications that redraw or reposition text may not produce the straightforward text you expect. + +Text received earlier in the same run can satisfy a wait. A successful wait consumes output through its match, leaving later text available for the next wait. Sending a string or special key clears the captured text before sending, so a following wait checks the response to that input rather than an earlier prompt. + +Prefer a specific readiness message over a broad match such as `$` or `ready`: command echo, a login banner, or unrelated output could otherwise satisfy the wait. A fixed **Wait** only measures time; it does not prove the server is ready. + +### Controlling port forwards + +Create the forward in the host's **Edit port forwards** screen first, then select it in the automation action. These actions control an existing forward; they do not create one or change its destination. Enabling an already enabled forward, or disabling an already disabled one, leaves it in the requested state. + +Forwarding actions require an **SSH** host. They cannot provide an SSH tunnel through Mosh or Telnet. A deleted forward leaves its automation action with a missing reference; edit that action to select a replacement or remove it. + +Remember that saved forwards normally start enabled. If a forward must stay off until a readiness check succeeds, configure it to start disabled in the port-forward list. Disabling it as the first automation action may still leave it briefly enabled during connection setup. + +Read the [port-forwarding guide](/guides/port-forwarding/) for server permissions, listening addresses, and testing. Enabling a listener does not prove the destination service is reachable. + +## Example: open a project and attach to tmux + +Suppose your shell prompt contains the distinctive text `workstation> ` and your project is in `$HOME/projects/my-app`. Add these actions: + +1. **Wait for string:** `workstation> `, with a suitable timeout and **Stop automation** on failure. +2. **Send string:** `cd "$HOME/projects/my-app" && tmux new-session -A -s work` +3. **Send special key:** Enter. + +This changes to the project directory and asks tmux to attach to an existing `work` session or create it. The server must have tmux installed. Replace the prompt and directory with ones that actually exist on your server. + +The `&&` prevents tmux from starting if changing directories fails. That condition is handled by the shell: ConnectBot's action list itself has no conditional branches or loops. For more complex logic, call a script on the server and use automation to start it and recognize its output. + +## Example: enable a tunnel after a readiness check + +Suppose you have a saved Local forward to a web service and a server-side `check-workspace` command that prints `WORKSPACE_OK` on its own line only when the service is ready. + +1. Configure the forward to start disabled. +2. **Wait for string:** your shell's distinctive prompt. +3. **Send string:** `check-workspace` +4. **Send special key:** Enter. +5. **Wait for string:** `(?m)^WORKSPACE_OK$`, with **Regular expression** enabled and **Stop automation** on failure. +6. **Enable port forward:** select your saved web-service forward. + +The line-specific match helps distinguish the result from the echoed command. `check-workspace` is an example of a command you supply on the server, not a built-in ConnectBot command. If the marker never arrives, the timeout stops the sequence before the tunnel is enabled. + +## Failures, progress, and cancellation + +Each action except **Disconnect** has an **On failure** setting: + +- **Stop automation** is the default. Later actions do not run after that action fails. +- **Continue** records the failure and moves to the next action. Use it for an optional step whose failure will not make later input unsafe or misleading. + +A text-wait timeout counts as a failure. Other failures include a closed terminal session, a missing forward, or a rejected forwarding request. Stopping automation normally leaves the connection open; **Disconnect** explicitly closes it and ends the sequence, even if more actions follow it. + +The terminal shows the current action number while automation runs and identifies an action that fails. **Cancel automation** stops the sequence while leaving the connection open. It does not undo commands already sent, stop a remote process, or reset a forward that was already changed. Use the terminal to inspect and clean up if needed. + +## Make routines reliable + +- **Test manually first.** Confirm the commands, prompts, and forwards work before automating them. Start with a short sequence and extend it gradually. +- **Use output to synchronize.** Slow networks and busy servers make fixed delays unreliable. Give text waits enough time and keep essential checks on **Stop automation**. +- **Avoid mixing manual typing with a running sequence.** Both send input to the same session, so typing while automation is waiting can change what later actions do. Cancel first when you need to take over. +- **Plan for reconnects.** A fresh terminal connection can repeat the actions. Prefer commands that reuse existing resources or check whether work has already been done. Mosh roaming without a new terminal session is not a fresh automation run. +- **Keep secrets out of saved text.** Action text is saved with the host's automation settings and is not a protected password field. Prefer [SSH keys](/guides/ssh-keys/) for authentication and handle sensitive prompts interactively. +- **Keep output manageable.** Automation has a bounded capture buffer. If too much output accumulates before it can be processed, the run stops instead of silently discarding it; an action's **Continue** setting does not bypass that safeguard. Avoid starting an unbounded log stream before a long wait. +- **Confirm completion before disconnecting.** Sending a command and immediately disconnecting can interrupt work. Wait for a reliable completion marker, or use a server-side tool designed to keep running after the session ends. + +## Implementation references + +The behavior described here is grounded in **ConnectBot v1.11.1**: + +- [AutomationEditorScreen.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/ui/screens/automation/AutomationEditorScreen.kt) and [AutomationEditorViewModel.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/ui/screens/automation/AutomationEditorViewModel.kt): editing, ordering, validation, and defaults. +- [AutomationRunner.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/service/automation/AutomationRunner.kt): sequential execution, matching, failures, and disconnect behavior. +- [AutomationOutput.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/service/automation/AutomationOutput.kt) and [AutomationRegex.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/service/automation/AutomationRegex.kt): output capture and regular-expression matching. +- [AutomationKeySupport.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/service/automation/AutomationKeySupport.kt) and [TerminalBridge.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/service/TerminalBridge.kt): supported keys, startup, cancellation, and forwarding control. diff --git a/src/content/guides/ssh-keys.mdx b/src/content/guides/ssh-keys.mdx new file mode 100644 index 00000000..ff6fd1ba --- /dev/null +++ b/src/content/guides/ssh-keys.mdx @@ -0,0 +1,75 @@ +--- +title: SSH key algorithms and biometric protection +--- + +# SSH key algorithms and biometric protection + +Written for **ConnectBot v1.11.1**. + +An SSH key pair lets you sign in without sending an account password to the server. Install the **public key** on the server and keep the **private key** on your device. ConnectBot uses the private key to sign a challenge that the server can verify. + +Two separate choices matter: **which algorithm your server accepts**, and **how ConnectBot stores and unlocks the private key**. + +## Compare the algorithms + +| Algorithm | What it means | Biometric-protected key generation in ConnectBot | +| --- | --- | --- | +| **RSA** | A widely supported algorithm with a configurable key size. Modern SSH servers can use RSA with SHA-256 or SHA-512 signatures. | Yes, when you enable fingerprint protection while generating the key. | +| **EC / ECDSA** | Elliptic-curve keys using the NIST P-256, P-384, or P-521 curves. ConnectBot labels this option **EC**; SSH uses ECDSA signatures. | Yes, when you enable fingerprint protection while generating the key. | +| **Ed25519** | A fixed-size elliptic-curve signature algorithm in the EdDSA family. It is a separate algorithm from ECDSA. | No. ConnectBot stores these as software keys. | +| **DSA** | An older algorithm, also called `ssh-dss`, retained for legacy compatibility. Avoid it for new keys: OpenSSH 10.0 removed DSA support. | No. ConnectBot stores these as software keys. | + +Your server must accept the algorithm you choose. Key sizes across algorithms are not directly comparable: a larger number does not automatically mean a stronger key. + +An RSA public key may start with `ssh-rsa` even when authentication uses SHA-256 or SHA-512. OpenSSH's retirement of the **RSA/SHA-1 signature algorithm** does not mean all RSA keys are obsolete. See the [OpenSSH 8.8 release notes](https://www.openssh.org/txt/release-8.8) and [OpenSSH 10.0 release notes](https://www.openssh.org/txt/release-10.0) for these compatibility changes. + +## A prompt is different from Keystore protection + +ConnectBot has three distinct controls: + +| Control | What it protects | +| --- | --- | +| **Key passphrase** | Encrypts a software private key stored by ConnectBot. You enter the passphrase to unlock it; the app then uses the unlocked key in memory. | +| **Confirm before use** | Asks whether ConnectBot may use the key. This is an app-level confirmation, not biometric authentication or Android Keystore storage. | +| **Fingerprint protection** | Generates an RSA or EC private key inside Android Keystore and requires biometric authentication for its use. The private key cannot be exported from Keystore. | + +**A password or confirmation prompt does not make a software key an Android Keystore key.** In the current implementation, Ed25519 and DSA can use passphrases and confirmation prompts, but they do not use ConnectBot's biometric unlock path. + +RSA and EC keys can also be ordinary software keys. Choosing one of these algorithms alone does not enable Keystore protection; you must enable **Fingerprint protection** when generating the key. Importing an existing RSA or EC private key does not automatically convert it into a Keystore key. + +## Generate a biometric-protected key + +1. Open ConnectBot's key manager and start generating a new key. +2. Choose **RSA** or **EC**. +3. Enable **Fingerprint protection**. This option requires a supported, enrolled biometric on your device. +4. Generate the key and copy its public key to the server account's authorized keys. +5. Select that key for your [SSH host](/guides/add-host/), then authenticate when prompted. + +ConnectBot currently configures a **30-second authentication window** for Keystore keys and removes its cached key reference when that window expires. This is not a promise of a fresh biometric prompt for every signing operation. + +### Keystore and hardware backing + +Android Keystore keeps the private key material outside ConnectBot's process and enforces its configured authentication requirements. ConnectBot tries StrongBox key generation first on supported Android versions, then falls back to the regular Android Keystore provider if that attempt fails. + +Hardware backing depends on the device, algorithm, and key size. **Android Keystore storage does not by itself guarantee StrongBox or other secure-hardware backing.** Android's [Keystore documentation](https://developer.android.com/privacy-and-security/keystore) explains this distinction and authentication windows. + +### Export, backup, and recovery + +Biometric-protected private keys are tied to their device. ConnectBot does not export or back them up, and it does not automatically unlock them at startup. The public key can still be shared with your server. + +Before relying on a device-bound key, keep another way to access your server. If you lose the device or its Keystore key becomes unavailable, you will need that access to authorize a replacement key. + +## Which should I choose? + +- **Want biometric-enforced, non-exportable storage?** Generate an RSA or EC key with fingerprint protection enabled, using parameters supported by your device and server. +- **Want a software key for a modern server?** Consider Ed25519 if your server accepts it, and protect the private key with a passphrase. +- **Need to move a key between devices?** Use an exportable software key and protect any copies. A Keystore private key cannot be transferred. +- **Connecting to an older server?** Check its accepted algorithms; use RSA where supported rather than creating a new DSA key. + +## Implementation references + +ConnectBot-specific behavior described here is for **v1.11.1**. These source references explain the implementation: + +- [BiometricKeyManager.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/util/BiometricKeyManager.kt): supported algorithms, Keystore generation, and authentication window. +- [GeneratePubkeyViewModel.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/ui/screens/generatepubkey/GeneratePubkeyViewModel.kt): software versus Keystore storage and backup settings. +- [SSH.kt](https://github.com/connectbot/connectbot/blob/v1.11.1/app/src/main/java/org/connectbot/transport/SSH.kt): separate biometric, passphrase, and confirmation paths. diff --git a/src/navigation/guides.ts b/src/navigation/guides.ts index 063354c2..64b9509a 100644 --- a/src/navigation/guides.ts +++ b/src/navigation/guides.ts @@ -12,6 +12,9 @@ export const guideGroups = [ { slug: 'raspberry-pi', title: 'Connect to a Raspberry Pi' }, { slug: 'linux-server', title: 'Administer a Linux server' }, { slug: 'mosh', title: 'Set up Mosh' }, + { slug: 'ssh-keys', title: 'SSH keys and biometric protection' }, + { slug: 'port-forwarding', title: 'Local, remote, and dynamic forwards' }, + { slug: 'post-login-automation', title: 'Post-login automation' }, ], }, {