Skip to content

Add onctl port-forward (pf), for every provider - #1169

Merged
cdalar merged 3 commits into
mainfrom
feat/port-forward
Oct 4, 2026
Merged

cdalar merged 3 commits into
mainfrom
feat/port-forward

Conversation

@cdalar

@cdalar cdalar commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Step 2 of docs/plans/boxes-provider.md, built for every provider rather than boxes only.

onctl port-forward my-vm 3000              # the VM's :3000 at localhost:3000
onctl pf my-vm 15432:5432 0:8080           # several at once; 0 = any free local port
onctl pf my-vm 5432:db.internal:5432       # somewhere only the VM can reach
onctl pf -p boxes my-box 8000              # boxes: through the tunnel

How it works

  • One forwarder for every provider: it runs over the ssh connection onctl already uses (direct-tcpip, like ssh -L), so the user, key, port and static-inventory handling match onctl ssh. For boxes, that ssh connection runs through the tunnel (cloud.Dialer, from Add the boxes provider: hosted microVMs, onctl login, ssh through the provider #1168).
  • Listens on 127.0.0.1 by default: --address 0.0.0.0 shares the forwards on your network.
  • Survives a pause: a paused VM doesn't close its old ssh connection, it just stops answering. So each forwarded connection first checks the ssh connection with a keepalive and reconnects if there's no reply within 5 seconds. For a box, reconnecting also resumes it.
  • A port with nothing listening is reported (connect failed ("Connection refused")) and doesn't trigger a reconnect.
  • New cloud.KeyAuthorizer: lets a provider put the key on an existing VM itself, so pf works on a box made in the dashboard. Boxes implements it.
  • pf alias: kubectl has no built-in alias for port-forward; it's offered here alongside the full name.

Also: onctl images leaves out columns that no image fills in. For boxes, that drops the three empty Hetzner-only columns (TYPE, OS FLAVOR, OS VERSION).

Tests

  • The spec parser (REMOTE, LOCAL:REMOTE, LOCAL:HOST:REMOTE, plus invalid forms).
  • An in-process ssh server serving direct-tcpip, checking:
    • one ssh connection carries many forwarded connections;
    • reconnecting after the connection drops;
    • a refused port is reported without reconnecting;
    • a connection that goes silent without closing is replaced within the keepalive timeout.
  • go test -race -shuffle=on ./... and golangci-lint are clean.

Live, against boxctl.io (throwaway boxes, destroyed afterwards):

  • two forwards at once;
  • the box's sshd banner comes through the forward;
  • a closed port is reported;
  • after pause -f, the next connection resumed the box and got through in 6.3s. Before this fix, that case hung.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U8g3mHkS6JmtYVYc4AjcV4

Forwards local ports to ports on a VM over the ssh connection onctl
already uses -- ssh -L without working out the user, key, address and
port -- so it works for every provider, and through the tunnel for
boxes (which have no address of their own).

Forwards are REMOTE, LOCAL:REMOTE (0 picks a free local port) or
LOCAL:HOST:REMOTE (somewhere only the VM can reach), on 127.0.0.1 unless
--address says otherwise. One ssh connection carries them all; before
each forwarded connection it's checked with a keepalive, because a VM
that was paused doesn't close its old connection, it just stops
answering. A dead one is replaced, which resumes a paused box.

cloud.KeyAuthorizer lets a provider put the key on an existing VM
itself, so port-forward works on a box made in the dashboard; boxes
implements it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8g3mHkS6JmtYVYc4AjcV4
@cdalar
cdalar enabled auto-merge October 4, 2026 21:26
cdalar and others added 2 commits October 4, 2026 23:28
The counter is bumped by the forwarder's goroutine and read by the test,
which the race detector flagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8g3mHkS6JmtYVYc4AjcV4
Boxes images have only a name and a description, so the table showed
three empty columns (TYPE, OS FLAVOR, OS VERSION) for them. Hetzner's
table is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U8g3mHkS6JmtYVYc4AjcV4
@codecov

codecov Bot commented Oct 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.83721% with 88 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.09%. Comparing base (1d1f094) to head (24a6178).

Files with missing lines Patch % Lines
cmd/portforward.go 5.88% 64 Missing ⚠️
cmd/common.go 0.00% 12 Missing ⚠️
internal/tools/forward.go 88.57% 8 Missing ⚠️
pkg/cloud/boxes.go 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1169      +/-   ##
==========================================
+ Coverage   38.81%   39.09%   +0.28%     
==========================================
  Files          57       59       +2     
  Lines        6073     6244     +171     
==========================================
+ Hits         2357     2441      +84     
- Misses       3716     3803      +87     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cdalar
cdalar merged commit 3d7097f into main Oct 4, 2026
14 checks passed
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

⌂ 换网址