Skip to content

feat: port knocking before SSH connect (#550) - #569

Merged
kipavy merged 22 commits into
devfrom
feat/550-port-knocking
Oct 7, 2026
Merged

kipavy merged 22 commits into
devfrom
feat/550-port-knocking

Conversation

@kipavy

@kipavy kipavy commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Closes #550.

Hosts can now be protected by port knocking. Voltius sends a configured TCP/UDP knock sequence before it dials SSH, so a router that keeps SSH closed until it sees a knock (MikroTik, knockd) works like any other saved host. Hosts without knocking behave as before.

Requires VoltiusApp/server#100 to be deployed first. Until it is, saving a knock sequence on a team-vault host fails.

What's new

Port Knocking panel (host form → Advanced, under Hosts Chaining)

  • Turn knocking on or off, and list the ports in order. Each port is TCP or UDP; drag to reorder, up to 16 ports.
  • Timing: the delay between knocks (default 200 ms), the wait after the last knock (default 500 ms), and an optional firewall window in seconds.
  • Inline checks:
    • no ports while knocking is on;
    • a port outside 1–65535;
    • a UDP step on a host whose proxy is SOCKS5/HTTP/HTTPS (error) or the system proxy (warning).
  • With jump hosts, a note names the bastion that gets knocked.

When it knocks

  • Before every SSH first-hop dial: terminal, SFTP, remote commands (key install, distro detection) and persistent-session cleanup.
  • Voltius knocks the host it dials directly: the target, or the first saved jump host, using that host's own knock settings. This is the same rule as the per-host proxy.
  • TCP knocks go through the host's proxy, so the firewall allow-lists the right source IP. UDP through a proxy is refused with a translated error.
  • Retries knock again. Only one sequence runs at a time per host, so tabs reconnecting together don't interleave their knocks.
  • The terminal's connect screen shows a "Port knock" step.

Reachability

  • A knock-protected host is only probed inside the firewall window after a Voltius knock.
  • Outside that window, its card shows a hollow dot and a lock instead of "down". Inside it, it shows the latency with an open lock.
  • The probe never knocks and never reads the sequence.
  • Plugins see these hosts as unknown; the plugin API is unchanged.

Secrets

  • The port sequence is stored with the host's secrets as knock_sequence:<id>, the same way as the proxy password. On/off, timings and the window are ordinary host settings.
  • In the form it loads only for people allowed to view secrets.
  • It's included in exports only when secrets are exported.
  • The form never saves a half-edited sequence. A bad stored value gives a translated error at connect.

Session commands

  • Pre/Post Command get a "Session commands" heading, the placeholders "Session pre-command" / "Session post-command", and a hint saying when they run. The stored fields are unchanged.

Also in this PR

  • The list of a host's secrets is now written in one table instead of four places.
  • The draggable numbered row, slide-over header/wrapper, add button, drill-in row, section label and hint are shared components.
  • Hosts Chaining, Environment Variables and snippet steps were pixel-checked against dev and are identical.
  • Unlinking a key from a host no longer wipes the host's proxy, jump hosts, environment variables and knock settings.
  • Direct SSH/WebDAV dials are now bounded by the 15 s proxy handshake timeout instead of the OS's TCP connect timeout.

Known trade-offs and follow-ups

  • A knock through a proxy to a DROP-filtered port usually adds about 5 s per connect, and again on each retry.
  • After the window closes, a host with an open terminal still shows "Knock window open", because its latency comes from that session.
  • The connect screen's knock detail shows just the port count.
  • The "ms"/"s" units aren't translated.
  • Someone not allowed to view secrets sees an empty sequence area with no explanation.
  • Viewing the sequence isn't recorded in the team audit log.

Testing

  • Rust: cargo fmt --check, clippy -D warnings, and cargo test (824 passed). New tests cover:
    • knock order, timing and UDP payload;
    • UDP refused through a proxy;
    • TCP through SOCKS5, and through a slow proxy;
    • no interleaving between concurrent knocks;
    • the ledger and the ping gate;
    • knocking before the SSH dial, and the "(after port knock)" error suffix;
    • port_knock storage and clocks.
  • TS: tsc --noEmit clean, and vitest passes for all of src and tests/. The hooks/utils/plugins/i18n group once exited without a summary while running after another group under load; on its own it passes 218/218 files.
  • Live, on a dev build against a test sshd:
    • the three knocks arrive at the server (UDP 1 byte, both TCP ports accepted), and the connect succeeds;
    • the card states before, inside and after the 60 s window;
    • SOCKS5 + UDP shows the error in the panel and on connect;
    • a partial host update keeps port_knock.

@kipavy
kipavy merged commit 86941cf into dev Oct 7, 2026
4 checks passed
@kipavy
kipavy deleted the feat/550-port-knocking branch October 7, 2026 22:52
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.

1 participant