Skip to content

Connection Commands

Mehdi edited this page Sep 21, 2026 · 2 revisions

Connection Commands & Port Knocking

SSH Pilot can open the way to a host before it connects, and run OpenSSH commands around the session. This is configured in the Commands tab of the Connection Editor (right-click a connection > Edit Connection > Commands).

The tab has two separate groups:

  • Port Knocking and Pre-Connect — what SSH Pilot runs itself, before OpenSSH is involved. Choose one: a built-in port knock sequence, or a shell command (fwknop, VPN dial-up, cloud firewall rule). This is the only part that applies to non-SSH protocols that still dial out (e.g. Docker over ssh://, Mosh).
  • SSH Commands — OpenSSH directives, sent to OpenSSH: Local Command (runs locally after connecting) and Remote Command (runs on the host upon login). These are hidden for protocols SSH Pilot does not build an ssh command for.

Commands tab: Port Knocking and Pre-Connect with knock sequence and command choices
Commands tab: pick a port knock sequence or run a pre-connect command, then test it


Port Knocking and Pre-Connect: choose one

The group header says it plainly: "Opens the way to the host before SSH Pilot connects. Choose one."

Two radio options, mutually exclusive:

Option What it is Needs installed?
Port knock sequence Ports to reach for, in order, e.g. 7000,8000,9000. Sent by SSH Pilot itself over plain sockets. Nothing. Works in the Flatpak.
Run a command Any shell line that has to run first — fwknop, a VPN dial-up, or a knock and a VPN together in one line. Whatever binary the command uses (fwknop, tailscale, aws, …). Under Flatpak it runs on the host via flatpak-spawn --host.

Both halves stay saved when you switch, so comparing the two and switching back destroys nothing. Only the selected one runs.

Port knock sequence (built-in, recommended for knockd)

Enter the sequence in knock(1) syntax so it can be pasted from existing notes or knockd.conf:

  • Separators are commas or whitespace: 7000,8000,9000 or 7000 8000 9000.
  • Protocol defaults to TCP. Add :udp to a port for UDP: 7000:udp,8000,9000 (:tcp is also accepted explicitly).
  • Up to 32 ports. An empty field means "no knock".
  • A sequence that will not parse is marked with an error highlight while you can still fix it. Pressing Test gives the precise reason.

There is deliberately no delay setting and no pause after the last knock — SSH dials immediately, and the SSH SYN's own retransmit covers the firewall's rule-insertion race. The daemon paces knocks ~0.2 s apart internally so the firewall's packet log stays ordered.

Note

A knock has no acknowledgement: a knocked port is supposed to be silent (DROPped or refused). "Sent" only means the packets reached the network. Whether the firewall opened is only knowable by connecting.

Run a command (the escape hatch)

Anything else that has to run first. Typical contents:

fwknop -n %h
tailscale up --accept-routes
aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 22 --cidr $(curl -s https://checkip.amazonaws.com)/32
  • Runs through a login shell (sh -lc), so PATH additions (/opt/homebrew/bin, /usr/local/bin) and expansions like $(which wget) work.
  • Tokens are expanded before the command runs (shell-quoted, so a space in a hostname stays a hostname):
    • %h — host, %p — port (defaults to 22 when the connection names none), %u — user.
    • %% is a literal percent; an unknown %x is left alone, so date +%H survives.
    • This avoids hardcoding a hostname that rots the first time the connection is re-pointed.
  • fwknop stays a command on purpose: Single Packet Authorization is a real encrypted protocol with replay protection, not a port sequence, and cannot be reimplemented as knocks.

Test button

Test tries whichever half is currently selected, using the host / user / port fields as they stand (not the last saved values), through the same daemon path a real connection uses. The result appears beside the button:

  • Knock: Sent in 0.6s. (packets went out — see the note above) or Could not be sent.
  • Command: Ran successfully in 0.4s., Exited with status 1 after 0.4s., Timed out after 30.0s., or Could not be started. — plus the command's own output when there is any.
  • Editing either field clears a stale result.

Timeout

Seconds before the command is stopped. 0 uses the app default.

  • Range 0–3600 s; 0 follows the app default (30 s).
  • Applies to Run a command only (it dims with the command field) — a knock is a handful of packets, not a process.

Do not connect if pre-connect command fails

Off by default.

  • Off: SSH Pilot warns (toast + terminal notice) and connects anyway, so the SSH error is the one you see. A pre-step veto tells you far less than OpenSSH's own diagnostics, which is why this is the default.
  • On: a failed knock or command refuses the launch — terminal, SFTP, forward, SCP, ssh-copy-id, and remote-command launches alike.
  • A failing run never strands you on a bug: an internal fault in the pre-connect step lets the launch continue.

How it runs under the hood

  • Stored as app metadata (connections.json: pre_command, pre_command_knock, pre_command_mode, pre_command_timeout, pre_command_abort) — not an SSH directive, so it is not written into ~/.ssh/config and applies to every protocol.
  • Runs for every launch kind, not just terminal tabs: terminal, SFTP browsing, port forwards, SCP transfers, ssh-copy-id, and Host Info probes all trigger it. That is why SFTP to a knock-gated host works.
  • Serialized per connection and coalesced (~5 s window): opening three tabs at once knocks once instead of firing three interleaved sequences that some knockd configurations score as a failure.
  • Vault/credential safety: if the connection uses a password or key in a locked vault (Bitwarden, KeePass, master password), SSH Pilot prompts before running the pre-connect step, so time spent typing does not eat a short firewall window.
  • Status text follows the running half: Sending the port knock… vs Running pre-connect command…; a failure names the half that failed rather than blaming a command you never configured.

Port Knocking & Single Packet Authorization (SPA)

Port knocking protects SSH servers from internet-wide scans by keeping port 22 closed until a valid knock sequence or encrypted packet is received.

Example 1: Classic port knocking (built-in, no knock tool)

Your server runs knockd and requires TCP 7000, 8000, 9000 to open port 22:

  1. Select Port knock sequence.
  2. Enter 7000,8000,9000.
  3. Press Test — expect Sent in …s., then connect.

Mixed UDP/TCP:

7000:udp,8000,9000

Tip

If the firewall needs a moment after the knock, do not add sleep to a knock sequence — there is nowhere to put it. Just connect: the SSH handshake retries while the rule lands. sleep only applies to Run a command lines, e.g. fwknop … && sleep 1.

Example 2: Single Packet Authorization (SPA) with fwknop

Select Run a command and enter one of:

fwknop -n %h
fwknop -n myserver
fwknop -n myserver --wget-cmd "$(which wget)"

Example 3: Cloud Security Group Authorization (AWS / Hetzner / DigitalOcean)

Select Run a command. Whitelist your current dynamic IP before connecting:

AWS EC2 Security Group:

aws ec2 authorize-security-group-ingress --group-id sg-0123456789abcdef0 --protocol tcp --port 22 --cidr $(curl -s https://checkip.amazonaws.com)/32

Hetzner Cloud Firewall:

hcloud firewall add-rule my-firewall --direction in --protocol tcp --port 22 --source-ips $(curl -s https://ipinfo.io/ip)/32

Example 4: VPN or Mesh Network Dial-up

Select Run a command. Ensure the tunnel is up before connecting:

Tailscale:

tailscale up --accept-routes

WireGuard:

sudo wg-quick up wg0

Need a knock and a VPN? Write both in the one command line — that combination is exactly what the command half is for.


Local Command

The Local Command field corresponds to the OpenSSH LocalCommand option. It runs on your local workstation after the SSH connection has successfully connected.

Requirements

OpenSSH requires PermitLocalCommand yes to execute local commands. SSH Pilot configures this automatically when a Local Command is specified.

In ~/.ssh/config:

Host my-server
    HostName 198.51.100.25
    PermitLocalCommand yes
    LocalCommand notify-send "Connected to %h"

Common Use Cases

  • Desktop Notifications:
    notify-send "SSH Pilot" "Connected to %h as %u"
  • Open Local Browser: If you have a local port forward configured (e.g. 8080:localhost:8080), you can launch your browser automatically:
    xdg-open http://localhost:8080

OpenSSH supports tokens in LocalCommand:

  • %h -- Remote hostname
  • %p -- Remote port
  • %r -- Remote username
  • %u -- Local username

Remote Command

The Remote Command field corresponds to the OpenSSH RemoteCommand option. It runs on the remote server immediately upon logging in.

In ~/.ssh/config:

Host dev-box
    HostName 198.51.100.25
    RemoteCommand tmux new -A -s main
    RequestTTY yes

Common Use Cases

  • Attach or Create a tmux Session:
    tmux new -A -s work
  • Run a System Monitoring Tool:
    btop
    # or
    htop
  • Launch a Custom Admin Menu:
    /usr/local/bin/admin-menu.sh

Note

When using interactive commands (such as tmux, htop, or shell menus), SSH Pilot automatically requests a pseudo-terminal (RequestTTY yes) so you can interact with the terminal normally.


Troubleshooting

"The port knock could not be sent"

Packets never reached the network — a down VPN, no route, or an unresolvable host. A refused or dropped knock is the normal case and reports success. Check:

  1. Can you resolve and route to the host at all?
  2. Is the VPN the knock must travel over actually up?
  3. Press Test — a knock failure there reproduces without opening a connection.

Pre-connect command is not finding the binary

  • Ensure the required tool (fwknop, aws, tailscale, etc.) is installed on your local computer (the built-in knock sequence needs nothing installed).
  • On macOS with Homebrew, the login shell (sh -lc) loads /opt/homebrew/bin and /usr/local/bin. Otherwise give the full path (e.g. /opt/homebrew/bin/fwknop).
  • Under Flatpak the command runs on the host — install the tool on the host, not in the sandbox.

The connection times out despite the knock

  1. Test first: press Test in the Commands tab, then try connecting so you know which half failed.
  2. For fwknop: if the client is behind NAT, ensure it discovers the public IP correctly (e.g. --resolve-http-ip).
  3. Check the gate switch: with Do not connect if pre-connect command fails on, a failing pre-connect refuses the launch; with it off (default), the SSH error is the primary signal.

Checking SSH Pilot logs

Pre-connect execution is traced in daemon.log (see docs/diagnostics.md in the repo). Launch with --debug/--diagnostics and look for:

port knock starting kind=terminal ports=3 [connection=… session=…]
port knock finished ports=3 duration_ms=412 kind=terminal
pre-connection command starting kind=terminal timeout_s=30 coalesce_s=5 [connection=… session=…]
pre-connection command finished exit=0 duration_ms=412 stdout_bytes=0 stderr_bytes=0 kind=terminal
  • kind= is the launch it belongs to: terminal, sftp, forward, scp, remote_command, or copy_id.
  • Knock ports, command text, and output are DEBUG-only (a sequence is a door code; a command line may carry a secret).

Related Guides

  • Connections -- Managing connection profiles and settings
  • Terminal -- Tabs, split view, and session handling
  • Security -- Credential management and secure storage
  • Port Forwarding -- Local, remote, and dynamic SOCKS tunnels

Clone this wiki locally